Compare commits

...

21 Commits

Author SHA1 Message Date
b8ac3e20e0 0.3.2 released, readme update 2025-12-15 09:04:14 +01:00
972b033be1 v0.3.2-SNAPSHOT: added android-specific code and implementation, including defaultNamedStorage() 2025-12-15 08:39:25 +01:00
1a46c8cab3 v0.3.0 released: kotlin 2.2.21 breaking changes with Instant/Clock, etc. adopted in a compatible way 2025-12-14 13:23:48 +01:00
5357b05f4a fixed publishing for newer kotlin 2025-09-25 18:08:48 +04:00
e7f44bb5a0 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	build.gradle.kts
2025-09-25 10:32:20 +04:00
01d9e0239e added more tools, start shifting to kotlin 2.2.0 2025-09-25 10:29:47 +04:00
1ccb8ea8b3 new release, finishing 2025-03-26 22:47:37 +03:00
d8b7306b3e published to all platforms 2025-03-26 22:36:53 +03:00
a121f2c4a6 upgraded ms_stools 2025-03-16 22:28:50 +03:00
e33273d597 More ByteChink sugar 2025-03-08 16:00:01 +03:00
6ea003700f small improvements 2025-03-08 02:09:35 +03:00
38751b5df6 more docs 2025-01-18 04:28:11 +03:00
39c5f1f586 version 0.1.11 2024-12-26 15:12:13 +03:00
3f83e842fe !fix Long.encodeToHex bug, rewritten with new types
+fast BitSet set with enums support
2024-12-23 01:07:04 +03:00
f6d2422cc1 +added more useful collections 2024-12-22 12:22:51 +03:00
d29bf960aa - no caching on stored delegates, fix problens with memory storage connections
+ optStored now properly load/store only non-null values
* possible concurrent modification in KVStorage.clear fixed
2024-11-30 13:37:20 +07:00
2eb38e27ed +ByteChunk 2024-10-11 08:24:00 +07:00
a4cf3fe8ee readme fix 2024-09-01 19:38:06 +02:00
988974230d readme fix 2024-08-24 07:14:10 +02:00
10b21ab205 docs/tests addons 2024-08-24 07:13:38 +02:00
fc9d4c5070 v0.1.7 release for all platforms on kotlin 2.0.20 - important fix in wasmJS 2024-08-24 07:00:01 +02:00
33 changed files with 1782 additions and 585 deletions

3
.gitignore vendored
View File

@ -5,3 +5,6 @@
/gradle/wrapper/gradle-wrapper.properties /gradle/wrapper/gradle-wrapper.properties
/node_modules /node_modules
.kotlin .kotlin
/.gigaide/gigaide.properties
/java_pid40366.hprof
/local.properties

View File

@ -4,16 +4,15 @@ Multiplatform binary tools collection, including portable serialization of the c
many useful tools to work with binary data, like CRC family checksums, dumps, etc. It works well also in the browser and many useful tools to work with binary data, like CRC family checksums, dumps, etc. It works well also in the browser and
in native targets. in native targets.
# Recent changes # Important note
- 0.1.6 add many useful features, added support to wasmJS and all other platforms. Note to wasmJS: it appears to be a bug in wasm compiler so BipackDecoder could cause wasm loading problem. Currently published version 0.3.2 for all platform is fully compatible with breaking kotlinx.datetime/kotlin.time migration as of Kotlin 2.2.21 and is __a recommended version to use__.
- 0.1.1: added serialized KVStorage with handy implementation on JVM and JS platforms and some required synchronization Sorry for inconveniences, it is all caused by strange ideas of the Kotlin team.
tools.
-
- 0.1.0: uses modern kotlin 1.9.*, fixes problem with singleton or empty/object serialization
The last 1.8-based version is 0.0.8. Some fixes are not yet backported to it pls leave an issue of needed. # Documentation
Aside of the samples in this readme please see [library documentation](https://code.sergeych.net/docs/mp_bintools/).
# Usage # Usage
@ -31,7 +30,7 @@ And add dependency to the proper place in your project like this:
```kotlin ```kotlin
dependencies { dependencies {
// ... // ...
implementation("net.sergeych:mp_bintools:0.1.0") implementation("net.sergeych:mp_bintools:0.1.12")
} }
``` ```

View File

@ -1,31 +1,29 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins { plugins {
kotlin("multiplatform") version "2.0.0" kotlin("multiplatform") version "2.2.21"
kotlin("plugin.serialization") version "2.0.0" kotlin("plugin.serialization") version "2.2.21"
id("org.jetbrains.dokka") version "1.9.20" id("org.jetbrains.dokka") version "1.9.20"
id("com.android.library") version "8.7.2"
`maven-publish` `maven-publish`
} }
val serialization_version = "1.6.5-SNAPSHOT"
group = "net.sergeych" group = "net.sergeych"
version = "0.1.6" version = "0.3.2"
repositories { repositories {
google()
mavenCentral() mavenCentral()
mavenLocal() mavenLocal()
maven("https://maven.universablockchain.com/") maven("https://maven.universablockchain.com/")
} }
kotlin { kotlin {
jvmToolchain(8) jvmToolchain(17)
jvm { jvm()
// compilations.all { androidTarget() {
// kotlinOptions.jvmTarget = "1.8" // Ensure Android variant is published to Maven so consumers get androidMain APIs
// } publishLibraryVariants("release")
withJava()
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
} }
js { js {
browser() browser()
@ -39,8 +37,8 @@ kotlin {
iosSimulatorArm64() iosSimulatorArm64()
linuxX64() linuxX64()
linuxArm64() linuxArm64()
mingwX64()
@OptIn(ExperimentalWasmDsl::class)
wasmJs { wasmJs {
browser() browser()
binaries.executable() binaries.executable()
@ -58,14 +56,15 @@ kotlin {
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi") languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes") languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.contracts.ExperimentalContracts") languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
languageSettings.optIn("kotlin.time.ExperimentalTime")
} }
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
// this is actually a bug: we need only the core, but bare core causes strange errors // this is actually a bug: we need only the core, but bare core causes strange errors
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
api("net.sergeych:mp_stools:[1.4.7,)") api("net.sergeych:mp_stools:[1.6.3,)")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
} }
} }
val nativeMain by creating { val nativeMain by creating {
@ -83,10 +82,17 @@ kotlin {
val commonTest by getting { val commonTest by getting {
dependencies { dependencies {
implementation(kotlin("test")) implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
} }
} }
val jvmMain by getting val jvmMain by getting
val jvmTest by getting val jvmTest by getting
val androidMain by getting {
dependencies {
// Android base64 and preferences are in the SDK; no extra deps required
}
}
val jsMain by getting { val jsMain by getting {
dependencies { dependencies {
} }
@ -95,29 +101,53 @@ kotlin {
// val nativeTest by getting // val nativeTest by getting
val wasmJsMain by getting { val wasmJsMain by getting {
dependencies { dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-browser-wasm-js:0.5.0")
}
}
val wasmJsTest by getting {
dependencies {
// implementation("org.jetbrains.kotlinx:kotlinx-browser:0.3")
} }
} }
val wasmJsTest by getting
} }
}
publishing {
val mavenToken by lazy {
File("${System.getProperty("user.home")}/.gitea_token").readText()
}
repositories {
maven {
credentials(HttpHeaderCredentials::class) {
name = "Authorization"
value = mavenToken
}
url = uri("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
authentication {
create("Authorization", HttpHeaderAuthentication::class)
}
}
}
}
android {
namespace = "net.sergeych.bintools"
compileSdk = 34
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
// Publish only release variant of the Android target so consumers (Android apps)
// resolve the platform artifact instead of common metadata, which is required
// to access Android-only APIs like net.sergeych.bintools.AndroidKV
publishing { publishing {
val mavenToken by lazy { singleVariant("release") {
File("${System.getProperty("user.home")}/.gitea_token").readText() withSourcesJar()
}
repositories {
maven {
credentials(HttpHeaderCredentials::class) {
name = "Authorization"
value = mavenToken
}
url = uri("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
authentication {
create("Authorization", HttpHeaderAuthentication::class)
}
}
} }
} }
} }
tasks.dokkaHtml.configure { tasks.dokkaHtml.configure {

View File

@ -1,2 +1,8 @@
kotlin.code.style=official kotlin.code.style=official
kotlin.js.compiler=ir kotlin.mpp.applyDefaultHierarchyTemplate=false
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
org.gradle.caching=true

41
gradlew vendored
View File

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -80,11 +80,13 @@ do
esac esac
done done
# This is normally unused APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# shellcheck disable=SC2034
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -131,29 +133,22 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
if ! command -v java >/dev/null 2>&1 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
case $MAX_FD in #( case $MAX_FD in #(
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -198,15 +193,11 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# Collect all arguments for the java command: # * put everything else in single quotes, so that it's not re-expanded.
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
@ -214,12 +205,6 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \ org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args. # Use "xargs" to parse quoted args.
# #
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. # With -n1 it outputs one arg per line, with the quotes and backslashes removed.

15
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@ -25,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%" == "" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -76,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd if "%ERRORLEVEL%"=="0" goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL% if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
if %EXIT_CODE% equ 0 set EXIT_CODE=1 exit /b 1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,21 @@
pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
mavenLocal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
mavenLocal()
maven("https://maven.universablockchain.com/")
}
}
rootProject.name = "mp_bintools" rootProject.name = "mp_bintools"

View File

@ -0,0 +1,65 @@
package net.sergeych.bintools
import android.content.Context
import android.content.SharedPreferences
import net.sergeych.bintools.AndroidKV.init
import net.sergeych.mp_tools.decodeBase64Compact
import net.sergeych.mp_tools.encodeToBase64Compact
/**
* Simple holder to provide Application [Context] for this library.
*
* Call [init] from your app (e.g., in `Application.onCreate`) before using
* [defaultNamedStorage].
*/
object AndroidKV {
@Volatile
private var appContext: Context? = null
fun init(context: Context) {
appContext = context.applicationContext
}
internal fun requireContext(): Context =
appContext ?: error("AndroidKV is not initialized. Call AndroidKV.init(applicationContext) first.")
}
/**
* Implementation of [KVStorage] backed by [SharedPreferences].
* Values are stored as base64-encoded strings to fit preferences capabilities.
*/
class SharedPreferencesKVStorage(
private val prefs: SharedPreferences,
keyPrefix: String = ""
) : KVStorage {
private val prefix = if (keyPrefix.isEmpty()) "" else "$keyPrefix:"
private fun k(key: String) = "$prefix$key"
override fun get(key: String): ByteArray? =
prefs.getString(k(key), null)?.decodeBase64Compact()
override fun set(key: String, value: ByteArray?) {
val e = prefs.edit()
val kk = k(key)
if (value == null) e.remove(kk)
else e.putString(kk, value.encodeToBase64Compact())
e.apply()
}
override val keys: Set<String>
get() {
val allKeys = prefs.all.keys
if (prefix.isEmpty()) return allKeys
return allKeys.filter { it.startsWith(prefix) }
.map { it.removePrefix(prefix) }
.toSet()
}
}
actual fun defaultNamedStorage(name: String): KVStorage {
val ctx = AndroidKV.requireContext()
val prefs = ctx.getSharedPreferences(name, Context.MODE_PRIVATE)
// We use separate preferences file per name, so prefix can be empty
return SharedPreferencesKVStorage(prefs)
}

View File

@ -0,0 +1,17 @@
package net.sergeych.synctools
import java.util.concurrent.locks.ReentrantLock
/**
* Android actual implementation mirrors JVM using [ReentrantLock].
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val access = ReentrantLock()
override fun lock() {
access.lock()
}
override fun unlock() {
access.unlock()
}
}

View File

@ -0,0 +1,22 @@
package net.sergeych.synctools
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private val access = Object()
actual fun await(milliseconds: Long): Boolean {
return synchronized(access) {
try {
access.wait(milliseconds)
true
} catch (_: InterruptedException) {
false
}
}
}
actual fun wakeUp() {
synchronized(access) { access.notifyAll() }
}
}

View File

@ -0,0 +1,98 @@
package net.sergeych.bintools
import kotlinx.serialization.Serializable
import net.sergeych.mp_tools.encodeToBase64Url
import kotlin.math.min
import kotlin.random.Random
/**
* Bytes sequence with comparison, concatenation, and string representation,
* could be used as hash keys for pure binary values, etc.
*/
@Suppress("unused")
@Serializable
class ByteChunk(val data: UByteArray): Comparable<ByteChunk> {
val size: Int get() = data.size
/**
* Per-byte comparison also of different length. From two chunks
* of different size but equal beginning, the shorter is considered
* the smaller.
*/
override fun compareTo(other: ByteChunk): Int {
val limit = min(size, other.size)
for( i in 0 ..< limit) {
val own = data[i]
val their = other.data[i]
if( own < their) return -1
else if( own > their) return 1
}
if( size < other.size ) return -1
if( size > other.size ) return 1
return 0
}
/**
* Equal chunks means content equality.
*/
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ByteChunk) return false
return data contentEquals other.data
}
/**
* Content-based hash code
*/
override fun hashCode(): Int {
return data.contentHashCode()
}
/**
* hex representation of data
*/
override fun toString(): String = base64
/**
* Hex encoded data
*/
val hex by lazy { data.encodeToHex() }
/**
* human-readable dump
*/
val dump by lazy { data.toDump() }
/**
* Lazy encode to base64 with url alphabet, without trailing fill '=' characters.
*/
val base64 by lazy { data.asByteArray().encodeToBase64Url() }
/**
* Lazy (cached) view of [data] as ByteArray
*/
val asByteArray: ByteArray by lazy { data.asByteArray() }
/**
* Concatenate two chunks and return new one
*/
operator fun plus(other: ByteChunk): ByteChunk = ByteChunk(data + other.data)
companion object {
fun fromHex(hex: String): ByteChunk = ByteChunk(hex.decodeHex().asUByteArray())
fun random(size: Int=16): ByteChunk = Random.nextBytes(size).asChunk()
}
}
/**
* Create the representation of this array as ByteChunk; it does not copy the data.
*/
fun ByteArray.asChunk() = ByteChunk(this.asUByteArray())
/**
* Create the representation of this array as ByteChunk; it does not copy the data.
*/
@Suppress("unused")
fun UByteArray.asChunk() = ByteChunk(this)

View File

@ -2,11 +2,18 @@ package net.sergeych.bintools
import net.sergeych.bipack.BipackDecoder import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder import net.sergeych.bipack.BipackEncoder
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.debug
import net.sergeych.synctools.AtomicCounter
import net.sergeych.synctools.ProtectedOp import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.WaitHandle import net.sergeych.synctools.WaitHandle
import net.sergeych.synctools.withLock import net.sergeych.synctools.withLock
class DataKVStorage(private val provider: DataProvider) : KVStorage { private val ac = AtomicCounter(0)
class DataKVStorage(private val provider: DataProvider) : KVStorage,
Loggable by LogTag("DKVS${ac.incrementAndGet()}") {
data class Lock(val name: String) { data class Lock(val name: String) {
private val exclusive = ProtectedOp() private val exclusive = ProtectedOp()
@ -20,8 +27,6 @@ class DataKVStorage(private val provider: DataProvider) : KVStorage {
exclusive.withLock { exclusive.withLock {
if (readerCount == 0) { if (readerCount == 0) {
return f() return f()
} else {
println("can't lock $this: count is $readerCount")
} }
} }
pulses.await() pulses.await()
@ -49,19 +54,16 @@ class DataKVStorage(private val provider: DataProvider) : KVStorage {
access.withLock { access.withLock {
// TODO: read keys // TODO: read keys
for (fn in provider.list()) { for (fn in provider.list()) {
println("Scanning: $fn") debug { "Scanning: $fn" }
if (fn.endsWith(".d")) { if (fn.endsWith(".d")) {
val id = fn.dropLast(2).toInt(16) val id = fn.dropLast(2).toInt(16)
println("found data record: $fn -> $id") debug { "found data record: $fn -> $id" }
val name = provider.read(fn) { BipackDecoder.decode<String>(it) } val name = provider.read(fn) { BipackDecoder.decode<String>(it) }
println("Key=$name")
keyIds[name] = id keyIds[name] = id
if (id > lastId) lastId = id if (id > lastId) lastId = id
} else println("ignoring record $fn") } else debug { "ignoring record $fn" }
} }
} }
println("initialized, ${keyIds.size} records found, lastId=$lastId")
} }
@ -97,7 +99,7 @@ class DataKVStorage(private val provider: DataProvider) : KVStorage {
lock.lockExclusive { provider.write(recordName(id), f) } lock.lockExclusive { provider.write(recordName(id), f) }
} }
fun deleteEntry(name: String) { private fun deleteEntry(name: String) {
// fast pre-check: // fast pre-check:
if (name !in keyIds) return if (name !in keyIds) return
// global lock: we can't now detect concurrent delete + write ops, so exclusive: // global lock: we can't now detect concurrent delete + write ops, so exclusive:

View File

@ -1,11 +1,10 @@
package net.sergeych.bintools package net.sergeych.bintools
import kotlinx.serialization.KSerializer
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import net.sergeych.bipack.BipackDecoder import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder import net.sergeych.bipack.BipackEncoder
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/** /**
@ -43,7 +42,7 @@ interface KVStorage {
* Default implementation uses [keys]. You may override it for performance * Default implementation uses [keys]. You may override it for performance
*/ */
fun clear() { fun clear() {
for (k in keys) this[k] = null for (k in keys.toList()) this[k] = null
} }
/** /**
@ -93,48 +92,50 @@ inline fun <reified T:Any>KVStorage.read(key: String): T? =
@Suppress("unused") @Suppress("unused")
inline fun <reified T:Any>KVStorage.load(key: String): T? = read(key) inline fun <reified T:Any>KVStorage.load(key: String): T? = read(key)
inline operator fun <reified T> KVStorage.invoke(defaultValue: T,overrideName: String? = null) = inline operator fun <reified T: Any> KVStorage.invoke(defaultValue: T,overrideName: String? = null) =
KVStorageDelegate<T>(this, typeOf<T>(), defaultValue, overrideName) KVStorageDelegate(this, serializer<T>(), defaultValue, overrideName)
inline fun <reified T> KVStorage.stored(defaultValue: T, overrideName: String? = null) = inline fun <reified T: Any> KVStorage.stored(defaultValue: T, overrideName: String? = null) =
KVStorageDelegate<T>(this, typeOf<T>(), defaultValue, overrideName) KVStorageDelegate(this, serializer<T>(), defaultValue, overrideName)
inline fun <reified T> KVStorage.optStored(overrideName: String? = null) = inline fun <reified T: Any> KVStorage.optStored(overrideName: String? = null) =
KVStorageDelegate<T?>(this, typeOf<T?>(), null, overrideName) KVStorageOptDelegate<T>(this, serializer<T>(),overrideName)
class KVStorageDelegate<T>( class KVStorageDelegate<T: Any>(
private val storage: KVStorage, private val storage: KVStorage,
type: KType, private val serializer: KSerializer<T>,
private val defaultValue: T, private val defaultValue: T,
private val overrideName: String? = null, private val overrideName: String? = null,
) { ) {
private fun name(property: KProperty<*>): String = overrideName ?: property.name private fun name(property: KProperty<*>): String = overrideName ?: property.name
private var cachedValue: T = defaultValue operator fun getValue(thisRef: Any?, property: KProperty<*>): T =
private var cacheReady = false storage.get(name(property))?.let { BipackDecoder.decode(serializer, it) }
private val serializer = serializer(type) ?: defaultValue
@Suppress("UNCHECKED_CAST")
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (cacheReady) return cachedValue
val data = storage.get(name(property))
println("Got data: ${data?.toDump()}")
if (data == null)
cachedValue = defaultValue
else
cachedValue = BipackDecoder.decode(data.toDataSource(), serializer) as T
cacheReady = true
return cachedValue
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
// if (!cacheReady || value != cachedValue) { storage[name(property)] = BipackEncoder.encode(serializer, value)
cachedValue = value }
cacheReady = true }
println("set ${name(property)} to ${BipackEncoder.encode(serializer, value).toDump()}")
class KVStorageOptDelegate<T: Any>(
private val storage: KVStorage,
private val serializer: KSerializer<T>,
private val overrideName: String? = null,
) {
private fun name(property: KProperty<*>): String = overrideName ?: property.name
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? =
storage.get(name(property))?.let{
BipackDecoder.decode(serializer, it)
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
if (value == null)
storage.delete(name(property))
else
storage[name(property)] = BipackEncoder.encode(serializer, value) storage[name(property)] = BipackEncoder.encode(serializer, value)
// }
} }
} }
@ -217,7 +218,19 @@ class MemoryKVStorage(copyFrom: KVStorage? = null) : KVStorage {
* - otherwise, the folder will be created in "`~/.local_storage`" parent directory * - otherwise, the folder will be created in "`~/.local_storage`" parent directory
* (which also will be created if needed). * (which also will be created if needed).
* *
* - For the native platorms it is not yet implemented (but will be soon). * - In Android (new!) it is implemented using preferences, but initializing is needed in
* main activity `onCreate`, somewhat like:
* ```kotlin
* override fun onCreate(savedInstanceState: Bundle?) {
* super.onCreate(savedInstanceState)
* // set the Context which Preferences will be used for
* // defaultNamedStorage
* net.sergeych.bintools.AndroidKV.init(this)
* }
* ```
* see also `AndroidKV` and `SharedPreferencesKVStorage` for Android
*
* - For the native platforms it is not yet implemented (but will be soon).
* *
* See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like, * See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like,
* and `FileDataProvider` class on JVM target. * and `FileDataProvider` class on JVM target.

View File

@ -8,6 +8,7 @@ package net.sergeych.bintools
* *
* Note that the cost, [MRUCache] is slower than [MutableMap]. * Note that the cost, [MRUCache] is slower than [MutableMap].
*/ */
@Deprecated("moved to net.sergeych.collections", ReplaceWith("net.sergeych.collections.MRUCache"))
class MRUCache<K,V>(val maxSize: Int, class MRUCache<K,V>(val maxSize: Int,
private val cache: LinkedHashMap<K,V> = LinkedHashMap() private val cache: LinkedHashMap<K,V> = LinkedHashMap()
): MutableMap<K,V> by cache { ): MutableMap<K,V> by cache {

View File

@ -84,13 +84,11 @@ private val hexDigits = "0123456789ABCDEF"
fun Long.encodeToHex(length: Int = 0): String { fun Long.encodeToHex(length: Int = 0): String {
var result = "" var result = ""
var value = this var value = this.toULong()
val end = if( value >= 0 ) 0L else -1L
// if (value < 0) throw IllegalArgumentException("cant convert to hex negative (ambiguous)")
do { do {
result = hexDigits[(value and 0x0f).toInt()] + result result = hexDigits[(value and 0x0fu).toInt()] + result
value = value shr 4 value = value shr 4
} while (value != end) } while (value != 0UL)
while (result.length < length) result = "0" + result while (result.length < length) result = "0" + result
return result return result
} }
@ -113,6 +111,8 @@ fun Collection<Byte>.encodeToHex(separator: String = " "): String = joinToString
fun ByteArray.toDump(wide: Boolean = false): String = toDumpLines(wide).joinToString("\n") fun ByteArray.toDump(wide: Boolean = false): String = toDumpLines(wide).joinToString("\n")
fun UByteArray.toDump(wide: Boolean = false): String = asByteArray().toDumpLines(wide).joinToString("\n")
fun ByteArray.toDumpLines(wide: Boolean = false): List<String> { fun ByteArray.toDumpLines(wide: Boolean = false): List<String> {
val lineSize = if (wide) 32 else 16 val lineSize = if (wide) 32 else 16

View File

@ -1,6 +1,5 @@
package net.sergeych.bipack package net.sergeych.bipack
import kotlinx.datetime.Instant
import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
@ -12,6 +11,7 @@ import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import net.sergeych.bintools.* import net.sergeych.bintools.*
import kotlin.time.Instant
/** /**
* Decode BiPack format. Note that it relies on [DataSource] so can throw [DataSource.EndOfData] * Decode BiPack format. Note that it relies on [DataSource] so can throw [DataSource.EndOfData]
@ -74,7 +74,7 @@ class BipackDecoder(
} }
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T { override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
return if (deserializer == Instant.serializer()) return if (deserializer == serializer<Instant>())
Instant.fromEpochMilliseconds(decodeLong()) as T Instant.fromEpochMilliseconds(decodeLong()) as T
else else
super.decodeSerializableValue(deserializer) super.decodeSerializableValue(deserializer)

View File

@ -1,6 +1,5 @@
package net.sergeych.bipack package net.sergeych.bipack
import kotlinx.datetime.Instant
import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.AbstractEncoder import kotlinx.serialization.encoding.AbstractEncoder
@ -9,6 +8,7 @@ import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import net.sergeych.bintools.* import net.sergeych.bintools.*
import kotlin.time.Instant
class BipackEncoder(val output: DataSink) : AbstractEncoder() { class BipackEncoder(val output: DataSink) : AbstractEncoder() {

View File

@ -3,22 +3,19 @@ package net.sergeych.bipack
import kotlinx.serialization.SerialInfo import kotlinx.serialization.SerialInfo
/** /**
* If this annotation is presented in some @Serializable class definition, its instances * To be used with [kotlinx.serialization.Serializable]. Allows serialized classes to be
* will be serialized with the leading number of fields. This allows extending class later * extended by _adding members with default initializers_ __to the end of the constructor list__.
* providing new parameters __to the end of the class__ and _with default values__.
* *
* __IMPORTANT NOTE__. Since version 0.0.7 it's been also possible to use default values * This annotation makes Bipack to insert fields count before the
* for non-serialized fields after the end-of-data. If the source reports it correctly, e.g. * serialized data. It then checks it on deserializing to fill not serialized fields will
* [net.sergeych.bintools.DataSource.isEnd] returns true, the unset fields are initialized * default values.
* with default value. This approach ___is not working when the loading instance is not the last
* in the deciding array!___, still it is useful to decode isolated objects. We recommend to
* use [Extendable] where needed and possible.
* *
* Whe deserializing such instances from previous version binaries, the new parameters * Note that since 0.0.7 the same behavior could be achieved by serializing each instance in the
* will get default values. * array as Bipack correctly processes end-of-data by filling missing fields with default values,
* using `Extendable` is more convenient and save some space, most of the time.
* *
* Serialized data of classes not market as ExtendableFormat could not be changed without * _Please note that without this annotation it could be impossible to deserialize old versions of
* breaking compatibility with existing serialized data. * the class, in particular, in array, inner fields, etc._
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@SerialInfo @SerialInfo

View File

@ -0,0 +1,342 @@
package net.sergeych.collections
import kotlinx.serialization.Serializable
import net.sergeych.bintools.encodeToHex
import net.sergeych.collections.BitSet.Companion.MAX_VALUE
/**
* Bitset is a serializable set of __positive__ integers represented as bits in long array.
* This ought to be more effective, and sure it is more effective in serialized form,
* as long as maximum stored number is not too big, We recommend limit of 10-20k.
*
* It limits hold values to [MAX_VALUE] anyway to avoid fast memory depletion
*
* It has optimized bitwise operation based versions of [retainAll] and [removeAll],
* [intersect] and [isEmpty], that are used if their arguments, where used, are `BitSet`
* instances. Also [equals] works faster with BitSet and BitSet.
*
* Use [bitSetOf] and [bitSetOfEnum] to simply create bitsets.
*
* It also contains syntax sugar to work with enums directly:
*
* - [includes] and [includesAll] to check that enum is in set
* - [insert], [insertAll], [delete], and [deleteAll] to manipulate enum values
* - [toEnumSet] to convert to set of enums
*
*/
@Serializable
class BitSet(private val bits: MutableList<Long> = mutableListOf()) : MutableSet<Int> {
fun set(element: Int, value: Boolean = true) = setBit(element, value)
fun clear(element: Int) = set(element, false)
operator fun plusAssign(element: Int) {
set(element)
}
operator fun plus(element: Int): BitSet = BitSet(bits.toMutableList()).apply { set(element) }
operator fun minusAssign(element: Int) {
clear(element)
}
operator fun minus(element: Int): BitSet = BitSet(bits.toMutableList()).apply { clear(element) }
private fun setBit(element: Int, value: Boolean): Boolean {
require(element >= 0, { "only positive numbers are allowed" })
require(element < MAX_VALUE, { "maximum value allowed is $MAX_VALUE" })
val offset = element shr 6
val bit = element % 64
return if (value) {
while (offset >= bits.size) bits.add(0)
val last = bits[offset] and masks[bit]
bits[offset] = bits[offset] or masks[bit]
last != 0L
} else {
if (offset < bits.size) {
// bigger, not existing means 0
val last = bits[offset] and masks[bit]
bits[offset] = bits[offset] and maskNot[bit]
last != 0L
} else {
// already 0: index not in bits:
false
}
}
}
private fun getBit(value: Int): Boolean {
val offset = value shr 6
if (offset >= bits.size) return false
val bit = value % 64
return (bits[offset] and masks[bit]) != 0L
}
override fun add(element: Int): Boolean = setBit(element, true)
override val size: Int
get() {
var count = 0
for (w in bits) {
for (m in masks) {
if ((w and m) != 0L) count++
}
}
return count
}
override fun addAll(elements: Collection<Int>): Boolean {
var added = false
for (i in elements) if (setBit(i, true)) added = true
return added
}
override fun clear() {
bits.clear()
}
override fun isEmpty(): Boolean {
if (bits.isEmpty()) return true
for (w in bits) if (w != 0L) return false
return true
}
fun toList(): List<Int> {
var value = 0
val result = mutableListOf<Int>()
for (w in bits) {
for (m in masks) {
if ((w and m) != 0L) result += value
value++
}
}
return result
}
fun toHex(): String = bits.toString() + " " + bits.joinToString(" ") { it.encodeToHex() }
override fun iterator(): MutableIterator<Int> = object : MutableIterator<Int> {
private val i = toList().iterator()
private var last: Int? = null
override operator fun hasNext() = i.hasNext()
override fun next(): Int = i.next().also { last = it }
override fun remove() {
last?.let { clear(it) } ?: IllegalStateException("hasNext() was not called")
}
}
private fun fastRetainAll(elements: BitSet): Boolean {
var result = false
for (i in bits.indices) {
if (i < elements.bits.size) {
val x = bits[i]
val y = x and elements.bits[i]
if (x != y) {
result = true
bits[i] = y
}
} else {
if (bits[i] != 0L) {
bits[i] = 0
result = true
}
}
}
return result
}
override fun retainAll(elements: Collection<Int>): Boolean {
return if (elements is BitSet)
fastRetainAll(elements)
else {
var value = 0
var result = false
for ((i, _w) in bits.withIndex()) {
var w = _w
for (m in masks) {
if ((w and m) != 0L && value !in elements) {
w = w and m.inv()
bits[i] = w
result = true
}
value++
}
}
result
}
}
override fun removeAll(elements: Collection<Int>): Boolean {
return if (elements is BitSet)
fastRemoveAll(elements)
else {
var value = 0
var result = false
for ((i, _w) in bits.withIndex()) {
var w = _w
for (m in masks) {
if ((w and m) != 0L && value in elements) {
w = w and m.inv()
bits[i] = w
result = true
}
value++
}
}
result
}
}
private fun fastRemoveAll(elements: BitSet): Boolean {
var result = false
for (i in bits.indices) {
if (i < elements.bits.size) {
val x = bits[i]
val y = x and elements.bits[i].inv()
if (x != y) {
bits[i] = y
result = true
}
}
}
println("fast2")
return result
}
override fun remove(element: Int): Boolean = setBit(element, false)
override fun containsAll(elements: Collection<Int>): Boolean {
for (e in elements) if (e !in this) return false
return true
}
fun toIntSet() = toList().toSet()
override fun contains(element: Int): Boolean = getBit(element)
/**
* Check that this set contains and ordinal of a given enum element.
*/
infix fun <E> includes(element: E)
where E : Enum<*> = contains(element.ordinal)
/**
* Check that this set contains all elements using its ordinals.
*/
infix fun <E> includesAll(elements: Collection<E>)
where E : Enum<*> = elements.all { it.ordinal in this }
fun intersect(other: Iterable<Int>): BitSet {
val result = toBitSet()
result.retainAll(other)
println("I: $this /\\ $other = $result")
return result
}
override fun toString(): String {
return toList().toString()
}
/**
* Checks that this set contains at least one element with ordinal
*/
infix inline fun <reified E> includesAny(elements: Collection<E>): Boolean
where E : Enum<E> {
val ords = elements.map { it.ordinal }.toBitSet()
return !ords.intersect(this).isEmpty()
}
/**
* Create an independent copy of this bitset
*/
fun toBitSet() = BitSet(bits.toMutableList())
inline fun <reified T> toEnumSet(): Set<T>
where T : Enum<T> {
val values = enumValues<T>()
val result = mutableSetOf<T>()
for (i in this) result += values[i]
return result
}
/**
* Insert an element of an enum by its ordinal, much like [add].
*
* @return `true` if the element has actually been added, `false` if
* BitSet was not modified.
*/
fun <E> insert(element: E): Boolean
where E : Enum<*> = add(element.ordinal)
/**
* Remove an element of an enum using its ordinal, much like [remove].
*
* @return `true` if the element has actually been removed, `false` if
* BitSet was not modified.
*/
fun <E> delete(element: E): Boolean
where E : Enum<*> = remove(element.ordinal)
/**
* Insert all elements using its ordinals, much like [addAll].
*
* @return `true` if at lease one element has actually been added, `false`
* if BitSet was not modified.
*/
fun <E> insertAll(element: Collection<E>): Boolean
where E : Enum<*> = addAll(element.map { it.ordinal })
/**
* Remove all the elements using its ordinals, much like [removeAll].
*
* @return `true` if at least one element has actually been removed, `false` if
* BitSet was not modified.
*/
fun <E> deleteAll(elements: Collection<E>): Boolean
where E : Enum<*> = removeAll(elements.map { it.ordinal })
/**
* Reduces storage size trying to compact storage. It might free some memory, depending
* on the platform implementation of lists and contents. Does not change stored values.
*/
fun compact() {
while( bits.isNotEmpty() && bits.last() == 0L ) bits.removeLast()
}
override fun hashCode(): Int = bits.hashCode()
override fun equals(other: Any?): Boolean {
if( other is BitSet ) {
compact(); other.compact()
return other.bits == this.bits
}
return toIntSet().equals(
if( other is Set<*>) other
else (other as Collection<*>).toSet()
)
}
companion object {
val masks = Array(64) { (1L shl it) }
val maskNot = masks.map { it.inv() }.toLongArray()
// limit size to ≈ 100kb
const val MAX_VALUE = 8_388_106
}
}
fun bitSetOf(vararg values: Int) = BitSet().apply {
for (i in values) add(i)
}
fun <E : Enum<*>> bitSetOfEnum(vararg values: E) =
BitSet().also {
for (v in values) it.add(v.ordinal)
}
fun Iterable<Int>.toBitSet(): BitSet = BitSet().also { it.addAll(this) }
fun IntArray.toBitSet(): BitSet = BitSet().also { it.addAll(this.asIterable()) }

View File

@ -0,0 +1,168 @@
package net.sergeych.collections
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.sergeych.mptools.withReentrantLock
import kotlin.time.Clock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Instant
/**
* MRU cache with expiration, with safe async concurrent access.
*
* Expired items are removed when accessing the map, when reading values or when putting
* it when [maxCapacity] is reached. See note about freeing resources below.
*
* It is much like [Map] and [MutableMap], but using suspend functions now limit usage of
* operator functions so we are not implementing it. Also, modification with [entries] is
* not allowed.
*
* Unlike [MRUCache], it drops expired values. Removing expired item is lazy, actual resource
* freeing could be delayed. To force actual removal use [cleanup].
*
* @param lifeTime how long the value should be kept
* @param maxCapacity if set, limits the capacity. Least Recent Used elements would be dropped
* to fit this parameter.
* @param onItemRemoved called when some item is removed for any reason (e.g. expiration or overwriting).
* Note that this call also suspends put variants until done
*/
class ExpirableAsyncCache<K, V>(
val lifeTime: Duration = 30.seconds,
val maxCapacity: Int? = null,
val onItemRemoved: (suspend (V) -> Unit)? = null
) {
class Slot<V>(
var value: V,
var lastUsedAt: Instant = Clock.System.now(),
)
private val access = Mutex()
private val cache = mutableMapOf<K, Slot<V>>()
suspend fun get(key: K): V? {
return access.withReentrantLock {
cache.get(key)?.let {
val now = Clock.System.now()
println("lifetime $key: ${now - it.lastUsedAt}")
if (now - it.lastUsedAt > lifeTime) {
cache.remove(key)
onItemRemoved?.invoke(it.value)
null
} else {
it.lastUsedAt = now
it.value
}
}
}
}
/**
* Put the value for key. Calls [onItemRemoved] if needed.
* @return previous value or null
*/
suspend fun put(key: K, value: V): V? {
// insert may replace existing item, so we do it first:
return access.withLock {
cache[key]?.let {
if (value != it.value)
onItemRemoved?.invoke(it.value)
val oldValue = it.value
it.value = value
it.lastUsedAt = Clock.System.now()
oldValue
} ?: run {
// overflow could be caused by put, so put first
cache.put(key, Slot(value))
// now check size
fixSize()
null
}
}
}
private suspend fun fixSize() {
maxCapacity?.let {
if (it >= cache.size) {
cache.remove(cache.minBy { it.value.lastUsedAt }.key)
?.also { onItemRemoved?.invoke(it.value) }
?.value
}
}
}
/**
* Remove all expired elements. This function is not needed unless you
* want to free resources associated with expired elements immediately.
*
* [onItemRemoved] is called for each removed item before returning.
*/
@Suppress("unused")
suspend fun cleanup() {
access.withLock {
val d = Clock.System.now()
for( e in cache.entries.toList()) {
if( d - e.value.lastUsedAt > lifeTime )
cache.remove(e.key)
}
}
}
suspend fun getOrDefault(key: K, value: V): V = get(key) ?: value
/**
* Atomically get or put value to the cache.
*
* If there is expired existing value, [onItemRemoved] will be called for it
* before assigning new value.
*/
suspend fun getOrPut(key: K, defaultValue: suspend () -> V): V {
return access.withLock {
cache[key]?.let {
if( Clock.System.now() - it.lastUsedAt > lifeTime) {
onItemRemoved?.invoke(it.value)
}
it.lastUsedAt = Clock.System.now()
it.value
} ?: run {
val v = defaultValue()
cache[key] = Slot(v)
fixSize()
v
}
}
}
data class Entry<K, V>(override val key: K, override val value: V) : Map.Entry<K, V>
val entries: Set<Map.Entry<K, V>>
get() = cache.entries.map { Entry(it.key, it.value.value) }.toSet()
val keys: Set<K>
get() = cache.keys
val size: Int
get() = cache.size
@Suppress("unused")
val values: Collection<V>
get() = cache.values.map { it.value }
@Suppress("unused")
fun isEmpty(): Boolean = cache.isEmpty()
@Suppress("unused")
fun containsValue(value: V): Boolean {
for (v in cache.values)
if (v.value == value) return true
return false
}
@Suppress("unused")
fun containsKey(key: K): Boolean = key in cache
operator fun contains(k: K): Boolean = k in cache
}

View File

@ -0,0 +1,64 @@
package net.sergeych.collections
/**
* Most Recently Used keys Cache.
* Maintains the specified size, removed least used elements on insertion. Element usage is
* when it is inserted, updated or accessed (with [get]). Least recently used (LRU) keys
* are automatically removed to maintain the [maxSize].
*
* Note that the cost, [MRUCache] is slower than [MutableMap].
*/
class MRUCache<K,V>(val maxSize: Int,
private val cache: LinkedHashMap<K,V> = LinkedHashMap()
): MutableMap<K,V> by cache {
private fun checkSize() {
while(cache.size > maxSize) {
cache.remove(cache.keys.first())
}
}
/**
* Put the [value] associated with [key] which becomes MRU whether it existed in the cache or was added now.
*
* If [size] == [maxSize] LRU key will be dropped.
*
* @return old value for the [key] or null
*/
override fun put(key: K, value: V): V? {
// we need it to become MRU, so we remove it to clear its position
val oldValue = cache.remove(key)
// now we always add, not update, so it will become MRU element:
cache.put(key,value).also { checkSize() }
return oldValue
}
/**
* Put all the key-value pairs, this is exactly same as calling [put] in the same
* order. Note that is the [from] map is not linked and its size is greater than
* [maxSize], some unpredictable keys will not be added. To be exact, only last
* [maxSize] keys will be added by the order providing by [from] map entries
* enumerator.
*
* If from is [LinkedHashMap] or like, onl
*/
override fun putAll(from: Map<out K, V>) {
// maybe we should optimize it not to add unnecessary first keys
for( e in from) {
put(e.key,e.value)
checkSize()
}
}
/**
* Get the value associated with the [key]. It makes the [key] a MRU (last to delete)
*/
override fun get(key: K): V? {
return cache[key]?.also {
cache.remove(key)
cache[key] = it
}
}
override fun toString(): String = cache.toString()
}

View File

@ -0,0 +1,199 @@
package net.sergeych.collections
/**
* Automatically mutable sorted list based on binary search and a given comparing function.
* To construct list of comparable elements use [invoke]. There is a secondary constructor
* to use with existing [Comparator] instance.
*
* While the sorted list is mutable, it does not implement `MutableList` because indexed
* assignments are not possible keeping sort order; use [add] instead.
*
* It is possible to store several equal values and retrieve them all. See [add] and [addIfNotExists].
*/
class SortedList<T: Any>(
private val list: MutableList<T> = mutableListOf(),
private val compare: (T,T) -> Int
)
: List<T> by list
{
@Suppress("unused")
constructor(list: MutableList<T>, comparator: Comparator<T>)
: this(list,{ a, b -> comparator.compare(a,b) })
private fun binarySearch(element: T): Int {
var low = 0
var high = this.size - 1
while (low <= high) {
val mid = (low + high).ushr(1) // unsigned shift right, equivalent to integer division of sum by 2
val midVal = list[mid]
val cmp = compare(element,midVal)
when {
cmp < 0 -> high = mid - 1
cmp > 0 -> low = mid + 1
else -> return mid // key found
}
}
return -(low + 1) // key not found, insertion point is -(low + 1)
}
/**
* Find any element equals to value using fast binary search.
*
* Note that if there are many elements that are equal to [value] using the [compare],
* it will return index of some of it. Use [findFirst] and [findLast] if needed.
*
* @return found value or null
*/
fun find(value: T): T? {
val i = binarySearch(value)
return if( i < 0 ) null else list[i]
}
/**
* Find all elements equal to the value.
* @return list of found elements, or an empty list.
*/
fun findAll(value: T): List<T> {
val result = mutableListOf<T>()
val start = binarySearch(value)
if( start >= 0) {
for( i in start ..< size ) {
val element = list[i]
if( compare(value, element) == 0 )
result += element
else
break
}
if( start > 0) {
for( i in (start-1) downTo 0) {
val element = list[i]
if (compare(value, element) == 0)
result += element
else
break
}
}
}
return result
}
/**
* Add all values. Duplicates will also be added.
*/
fun add(vararg values: T) {
for( value in values ) {
val i = binarySearch(value)
if (i >= 0)
list.add(i + 1, value)
else
list.add(-(i + 1), value)
}
}
/**
* Remove one element equals to value.
* @return true if the element has been removed
*/
fun remove(value: T): Boolean {
val i = binarySearch(value)
return if( i >= 0) {
list.removeAt(i)
true
}
else false
}
/**
* Remove element at index.
* @return element that has been removed
*/
@Suppress("unused")
fun removeAt(index: Int): T = list.removeAt(index)
/**
* Optimized, binary search based version.
* @returns index of the _first_ occurrence of the element, or -1
*/
override fun indexOf(element: T): Int {
var i = binarySearch(element)
if( i < 0 ) return -1
while( i > 0 && compare(element, list[i-1]) == 0) i--
return i
}
/**
* Optimized, binary search based version.
* @returns index of the _last_ occurrence of the element, or -1
*/
override fun lastIndexOf(element: T): Int {
var i = binarySearch(element)
if( i < 0 ) return -1
while( i < list.size && compare(element, list[i+1])==0) i++
return i
}
/**
* Optimized, binary search based, first occurrence of the element.
*
* Note that the order of 'equal' elements is unspecified, order of appearance is not kept.
*/
fun findFirst(element: T): T? {
val i = indexOf(element)
return if( i < 0 ) null else list[i]
}
/**
* Optimized, binary search based search of the last occurrence of the element
*
* Note that the order of 'equal' elements is unspecified, order of appearance is not kept.
*/
fun findLast(element: T): T? {
val i = lastIndexOf(element)
return if( i < 0 ) null else list[i]
}
/**
* Remove all elements equals to value.
* @return number of removed elements
*/
fun removeAll(value: T): Int {
var start = binarySearch(value)
var count = 0
while( start < size && compare(value, list[start]) == 0 ) {
list.removeAt(start)
count++
}
while( start > 0 && compare(value, list[--start]) == 0) {
list.removeAt(start)
count++
}
return count
}
override operator fun contains(element: T): Boolean = binarySearch(element) >= 0
/**
* Add a value if it is not yet in the list.
* @return true if the value was added and false if it is already in the list, and was not added.
*/
fun addIfNotExists(value: T): Boolean {
val i = binarySearch(value)
return if( i < 0) {
list.add(-(i + 1), value)
true
}
else false
}
companion object {
/**
* Construct list of elements from comparable instances.
*/
operator fun <T: Comparable<T>>invoke(vararg values: T): SortedList<T> =
SortedList(values.toList().sorted().toMutableList()) { a, b -> a.compareTo(b) }
}
}

View File

@ -0,0 +1,24 @@
package net.sergecyh.diwan.tools
import kotlin.time.Clock
import kotlin.time.Duration
import kotlin.time.Instant
/**
* Value with expiration.
*/
@Suppress("unused")
class Expiring<T>(
val value: T,
val expiresAt: Instant,
) {
constructor(value: T, expiresIn: Duration) : this(value, Clock.System.now() + expiresIn)
/**
* @return value if not expired, null otherwise
*/
fun valueOrNull(): T? = if( isExpired ) value else null
val isExpired: Boolean get() = expiresAt < Clock.System.now()
val isOk: Boolean get() = !isExpired
}

View File

@ -0,0 +1,29 @@
@file:Suppress("unused")
package net.sergecyh.diwan.tools
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* Experimental
*
* set of mutexes associated with keys `K`, so each key can have its own mutex
* used with [lock] or [withLock]
*/
class IndividualLock<K> {
private val access = Mutex()
private val locks = mutableMapOf<K, Mutex>()
suspend inline fun <T>withLock(key: K, block: ()->T): T = lock(key).use { block() }
suspend fun lock(key: K): AutoCloseable {
val m = access.withLock {
locks.getOrPut(key) { Mutex() }
}
m.lock()
return AutoCloseable { m.unlock() }
}
}

View File

@ -0,0 +1,44 @@
package net.sergecyh.diwan.tools
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* Coroutine-based unique "once-per-key" executor.
*
* When [invoke] is called, it checks that there is already invocation in progress
* and either wait for it to complete or start a new one.
* @param K key value, should have valid `hashCode` and `equals`, e.g., suitable to be a map key
*/
@Suppress("unused")
class OncePer<K> {
private val access = Mutex()
private val queue = mutableMapOf<K,CompletableDeferred<Any>>()
/**
* Execute [f] as unique-per-key [key]. If such invocation is in progress, it suspends than returns
* its result. If there is no invocation for such a key, start new invocation.
*
* __Important note__ all simultaneous invocation should have the same [R] type, or, at least, a type
* _castable to [R]_.
*/
suspend operator fun <R: Any>invoke(key: K, f: suspend ()->R ): R {
var mustStart = false
val d = access.withLock {
queue.getOrPut(key) {
CompletableDeferred<Any>().also { mustStart = true }
}
}
if( mustStart ) {
d.complete(f())
access.withLock {
queue.remove(key)
}
}
@Suppress("UNCHECKED_CAST")
return d.await() as R
}
}

View File

@ -0,0 +1,27 @@
package net.sergecyh.diwan.tools
import kotlin.time.Clock
import kotlin.time.Duration
import kotlin.time.Instant
/**
* Experimental.
*
* limit invocations rate of [invoke] to once per [minimalInterval] or less frequent.
* Note that it is not a debouncing, it just ignores too frequent calls!
*/
@Suppress("unused")
class RateLimiter(val minimalInterval: Duration) {
var lastExecutedAt = Instant.DISTANT_PAST
private set
/**
* invoke [f] if the last invocation was earlier than now minus [minimalInterval], otherwise
* do nothing.
* @return the value returned by [f] if it was actually invoked this time, null otherwise
*/
suspend operator fun <T> invoke(f: suspend () -> T): T? =
if (Clock.System.now() - lastExecutedAt > minimalInterval) {
f().also { lastExecutedAt = Clock.System.now() }
} else null
}

View File

@ -1,7 +1,5 @@
package bipack package bipack
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.sergeych.bintools.encodeToHex import net.sergeych.bintools.encodeToHex
@ -13,6 +11,8 @@ import kotlin.test.Test
import kotlin.test.assertContentEquals import kotlin.test.assertContentEquals
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.time.Clock
import kotlin.time.Instant
@Serializable @Serializable
data class Foobar1N(val bar: Int, val foo: Int = 117) data class Foobar1N(val bar: Int, val foo: Int = 117)
@ -30,6 +30,7 @@ data class Foobar2(val bar: Int, val foo: Int, val other: Int = -1)
@Framed @Framed
data class FoobarF1(val bar: Int, val foo: Int = 117) data class FoobarF1(val bar: Int, val foo: Int = 117)
@Suppress("unused")
@Serializable @Serializable
@Framed @Framed
@SerialName("bipack.FoobarF1") @SerialName("bipack.FoobarF1")
@ -440,4 +441,21 @@ class BipackEncoderTest {
println(y) println(y)
} }
@Serializable
data class T1(@Fixed val i: Byte)
@Test
fun testFixedByte() {
fun t1(i: Int) {
val packed = BipackEncoder.encode(T1(i.toByte()))
println(packed.toDump())
assertEquals(1, packed.size)
assertEquals(i, BipackDecoder.decode<T1>(packed).i.toInt())
}
t1(127)
t1(-127)
t1(1)
t1(-1)
}
} }

View File

@ -0,0 +1,60 @@
package bipack
import net.sergeych.bintools.*
import net.sergeych.collections.bitSetOf
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
class StorageTest {
@Test
fun storageTest3() {
val s1 = MemoryKVStorage()
val s2 = defaultNamedStorage("test_mp_bintools2")
s2.clear()
s1.write("foo", "bar")
s1.write("foo2", "bar2")
s2.write("foo", "foobar")
s2.write("bar", "buzz")
s2.write("reason", 42)
assertEquals("bar", s1.read("foo"))
assertEquals("bar2", s1.read("foo2"))
assertNull(s1.get("bar"))
val reason: Int? by s1.optStored()
assertNull(s1.get("reason"))
assertNull(reason)
s1.connectToStorage(s2)
// s1 overwrites s2!
assertEquals("bar", s1.read("foo"))
// don't change
assertEquals("bar2", s1.read("foo2"))
// pull from s1
assertEquals("buzz", s1.read("bar"))
assertEquals(42, s1.read("reason"))
assertEquals(42, reason)
}
@Test
fun bitSetEquityTest() {
val a = bitSetOf(1, 12)
val b = bitSetOf(12, 1)
assertEquals(a, b)
assertEquals(b, a)
a += 1230
assertFalse { a == b }
a -= 1230
assertEquals(a, b)
assertEquals(b, a)
}
}

View File

@ -0,0 +1,166 @@
package collections
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import net.sergeych.bintools.toDump
import net.sergeych.bipack.BipackEncoder
import net.sergeych.collections.*
import kotlin.test.*
import kotlin.time.Duration.Companion.milliseconds
class CollectionsTest {
@Test
fun testSortedList1() {
val a1 = SortedList(5, 4, 3, 9, 1)
assertTrue { 4 in a1 }
assertTrue { 14 !in a1 }
fun <T : Comparable<T>> test(x: SortedList<T>) {
var last: T? = null
for (i in x.toList()) {
if (last == null) last = i
else if (last > i) fail("invalid order: $last should be <= $i")
assertContains(x, i)
}
}
test(a1)
a1.add(11, 3, 2, 1, 0, 9, 22, -55, 0, 1, 0)
println(a1.toList())
assertEquals(listOf(-55, 0, 0, 0, 1, 1, 1, 2, 3, 3, 4, 5, 9, 9, 11, 22), a1.toList())
assertEquals(11, a1.find(11))
assertEquals(listOf(0, 0, 0), a1.findAll(0))
assertEquals(listOf(11), a1.findAll(11))
assertEquals(listOf(3, 3), a1.findAll(3))
assertTrue { a1.remove(3) }
assertEquals(listOf(3), a1.findAll(3))
assertTrue { a1.remove(3) }
assertEquals(listOf(), a1.findAll(3))
assertTrue { 3 !in a1 }
assertEquals(3, a1.findAll(1).size)
assertEquals(3, a1.removeAll(1))
assertTrue { 1 !in a1 }
assertEquals(listOf(-55, 0, 0, 0, 2, 4, 5, 9, 9, 11, 22), a1.toList())
}
@Test
fun expirableAsyncCacheTest() = runTest {
val removedValues = mutableSetOf<Int>()
val m = ExpirableAsyncCache<String, Int>(500.milliseconds) {
removedValues += it
}
m.put("one", 1)
m.put("two", 2)
assertTrue("one" in m)
assertTrue("two" in m)
assertEquals(1, m.get("one"))
assertEquals(2, m.get("two"))
assertTrue { removedValues.isEmpty() }
m.put("one", 11)
assertEquals(11, m.get("one"))
assertEquals(removedValues, setOf(1))
m.getOrDefault("two", 22)
assertEquals(2, m.get("two"))
m.getOrPut("two") { 222 }
assertEquals(2, m.get("two"))
m.getOrPut("three") { 3 }
assertEquals(3, m.get("three"))
// This sadly is not working
// delay(2000)
// advanceTimeBy(2000)
// delay(1000)
// assertNull(m.get("one"))
}
enum class Nn {
One, Two, Three
}
@Test
fun bitsetTest() {
fun checkAdd(vararg values: Int) {
val x = bitSetOf(*values)
println(":: ${values.toList()}: ${x.toHex()}")
assertEquals(values.toSet(), x.toIntSet())
for (i in values) {
assertTrue(i in x, "failed $i in ${values.toList()}")
}
}
checkAdd(0, 1, 2, 3)
val src = intArrayOf(31, 32, 33, 60, 61, 62, 63, 64, 65)
checkAdd(*src)
assertEquals(src.toSet(), src.toBitSet().toIntSet())
assertFalse { src.toSet() != src.toBitSet() }
assertFalse { src.toSet() + 17 == src.toBitSet() }
assertEquals(src.toBitSet() + 17, src.toSet() + 17, )
assertTrue { src.toSet() + 17 == src.toBitSet() + 17 }
assertTrue { src.toBitSet() + 17 == src.toBitSet() + 17 }
var y = src.toBitSet() + 2
assertTrue { y.retainAll(setOf(1, 3, 31, 32, 33)) }
assertEquals(setOf(31, 32, 33), y.toIntSet())
assertFalse { y.retainAll(setOf(1, 3, 31, 32, 33)) }
y = src.toBitSet() + 2
for (i in setOf(2, 31, 32, 33))
assertTrue(i in y, "failed $i in ${y.toList()}")
assertTrue { y.retainAll(bitSetOf(1, 3, 31, 32, 33)) }
assertEquals(setOf(31, 32, 33), y.toIntSet())
assertFalse { y.retainAll(setOf(1, 3, 31, 32, 33)) }
var z = src.toBitSet() + 2
assertTrue { z.removeAll(setOf(31, 65)) }
assertEquals(listOf(2, 32, 33, 60, 61, 62, 63, 64), z.toList())
assertFalse { z.removeAll(setOf(31, 65)) }
z = src.toBitSet() + 2
assertTrue { z.removeAll(bitSetOf(31, 65)) }
assertEquals(listOf(2, 32, 33, 60, 61, 62, 63, 64), z.toList())
assertFalse { z.removeAll(setOf(31, 65)) }
z = src.toBitSet() + 2
assertTrue { z.removeAll(bitSetOf(31, 32)) }
assertEquals(listOf(2, 33, 60, 61, 62, 63, 64, 65), z.toList())
assertFalse { z.removeAll(setOf(31, 4)) }
assertTrue {
BipackEncoder.encode(src.toSet()).size > BipackEncoder.encode(src.toBitSet()).size
}
assertFalse { z includes Nn.Two }
assertTrue { z includes Nn.Three }
assertTrue { z + 1 includesAll listOf(Nn.Three, Nn.Two) }
assertEquals(setOf(Nn.One, Nn.Three), bitSetOf(0, 2).toEnumSet())
assertTrue { z + 1 includesAny setOf(Nn.One, Nn.Two) }
}
@Test
fun bitsetEnumsTest() {
val a = bitSetOfEnum(Nn.One)
assertTrue { a includes Nn.One }
assertFalse { a includes Nn.Two }
assertFalse { a includes Nn.Three }
a.insert(Nn.Three)
assertEquals(setOf(Nn.One, Nn.Three), a.toEnumSet())
a.delete(Nn.One)
assertEquals(setOf(Nn.Three), a.toEnumSet())
a.insertAll(listOf(Nn.Two, Nn.Three))
assertEquals(setOf(Nn.Two, Nn.Three), a.toEnumSet())
assertTrue { a includesAll listOf(Nn.Two, Nn.Three) }
assertFalse { a includesAll listOf(Nn.One, Nn.Two) }
assertTrue { a includesAny listOf(Nn.One, Nn.Two) }
a.deleteAll(listOf(Nn.Two, Nn.Three, Nn.One))
assertTrue { a.isEmpty() }
assertTrue { a.toEnumSet<Nn>().isEmpty() }
}
}

View File

@ -0,0 +1,34 @@
@file:Suppress("unused")
package net.sergeych.synctools
import java.nio.channels.CompletionHandler
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* Helper class to handle Java continuation with Kotlin coroutines.
* Usage sample:
* ```kotlin
* val socket = withContext(Dispatchers.IO) {
* AsynchronousSocketChannel.open()
* }
* suspendCoroutine { cont ->
* socket.connect(address.socketAddress, cont, VoidCompletionHandler)
* }
* ```
*/
open class ContinuationHandler<T> : CompletionHandler<T, Continuation<T>> {
override fun completed(result: T, attachment: Continuation<T>) {
attachment.resume(result)
}
override fun failed(exc: Throwable, attachment: Continuation<T>) {
attachment.resumeWithException(exc)
}
}
object VoidCompletionHandler : ContinuationHandler<Void>()
object IntCompletionHandler : ContinuationHandler<Int>()

View File

@ -1,9 +0,0 @@
import kotlin.test.Test
class EmptyTest {
@Test
fun emptyTest() {
println("hello!")
}
}

View File

@ -5,10 +5,6 @@ import kotlin.test.assertNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
class StorageTest { class StorageTest {
@Test
fun emptyTest() {
println("hello")
}
@Test @Test
fun storageTest() { fun storageTest() {
@ -27,7 +23,6 @@ class StorageTest {
assertEquals(answer, 42) assertEquals(answer, 42)
answer = 43 answer = 43
println("----------------------------------------------------------------")
val s2 = defaultNamedStorage("test_mp_bintools") val s2 = defaultNamedStorage("test_mp_bintools")
val foo1 by s2.stored("?", "foo") val foo1 by s2.stored("?", "foo")
val answer1: Int? by s2.optStored("answer") val answer1: Int? by s2.optStored("answer")
@ -39,5 +34,4 @@ class StorageTest {
s2.write("test_$i", "payload_$i") s2.write("test_$i", "payload_$i")
} }
} }
} }