39 Commits
Author SHA1 Message Date
sergeych 1087a2c1c7 merge v0.2.2: kotlin 2.2.20 for all targets 2025-10-20 21:02:04 +02:00
sergeych ec91c0800d v0.1.8: kotlin 2.2.20 for all targets 2025-10-20 21:01:36 +02:00
sergeych 5357b05f4a fixed publishing for newer kotlin 2025-09-25 18:08:48 +04:00
sergeych e7f44bb5a0 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	build.gradle.kts
2025-09-25 10:32:20 +04:00
sergeych 01d9e0239e added more tools, start shifting to kotlin 2.2.0 2025-09-25 10:29:47 +04:00
sergeych 1ccb8ea8b3 new release, finishing 2025-03-26 22:47:37 +03:00
sergeych d8b7306b3e published to all platforms 2025-03-26 22:36:53 +03:00
sergeych a121f2c4a6 upgraded ms_stools 2025-03-16 22:28:50 +03:00
sergeych e33273d597 More ByteChink sugar 2025-03-08 16:00:01 +03:00
sergeych 6ea003700f small improvements 2025-03-08 02:09:35 +03:00
sergeych 38751b5df6 more docs 2025-01-18 04:28:11 +03:00
sergeych 39c5f1f586 version 0.1.11 2024-12-26 15:12:13 +03:00
sergeych 3f83e842fe !fix Long.encodeToHex bug, rewritten with new types
+fast BitSet set with enums support
2024-12-23 01:07:04 +03:00
sergeych f6d2422cc1 +added more useful collections 2024-12-22 12:22:51 +03:00
sergeych 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
sergeych 2eb38e27ed +ByteChunk 2024-10-11 08:24:00 +07:00
sergeych a4cf3fe8ee readme fix 2024-09-01 19:38:06 +02:00
sergeych 988974230d readme fix 2024-08-24 07:14:10 +02:00
sergeych 10b21ab205 docs/tests addons 2024-08-24 07:13:38 +02:00
sergeych 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
sergeych e07ebf564a v0.1.6 release for all platforms 2024-07-27 19:00:36 +02:00
sergeych a866dff852 docs publc 2024-07-24 22:50:54 +03:00
sergeych 904a91b8de more sugar & unsigned support 2024-06-30 12:50:52 +07:00
sergeych 1fd229fdb1 Added MRUCache 2024-06-29 10:32:03 +07:00
sergeych cc8c9ecc5d MP fixes: more platforms 2024-06-08 20:27:51 +07:00
sergeych a5f2128e1d added & published for all KMP targets (0.1.3) 2024-04-27 00:09:58 +02:00
sergeych 1c84f286d9 MP fixes 2024-04-23 21:49:57 +03:00
sergeych fa87a0f611 more ios/macos targets implementation 2024-03-15 19:22:36 +01:00
sergeych 87a0c14b85 0.1.1 published to platforms except apple 2024-02-26 12:47:24 +03:00
sergeych e9082af4de more KMP targets 2024-02-26 12:44:44 +03:00
sergeych 3c33eb3bd9 refactored packages, added more docs 2024-02-19 03:33:45 +03:00
sergeych babc3933eb 0.1.1-SNAPSHOT: storages, sync tools, docs 2024-02-18 22:52:36 +03:00
sergeych 3e6c487601 release 0.1.0: fixed bug with object/empty serialization, migrated to newest kotlin 2024-01-28 16:06:48 +03:00
sergeych 47b450c597 0.0.7: oendOfData support, correct decoding with default values past it 2023-12-24 00:39:38 +03:00
sergeych 06f8bc7ef2 release 0.0.6 2023-11-22 18:44:29 +03:00
sergeych 30bf5fefe9 more support for ubytearrays 2023-10-26 10:35:41 +03:00
sergeych b5db8bb8cc version bump in readme 2023-10-07 01:58:48 +01:00
sergeych 9eb7bd9e0e version bump 2023-10-07 01:55:59 +01:00
sergeych e1c78e2bbb 0.0.4-rc: fixed a bug in variable-size short field (rare situation and most often masked). 2023-10-07 01:50:26 +01:00
74 changed files with 3878 additions and 573 deletions

No files matched your search

+3
View File
@@ -4,3 +4,6 @@
/gradle/wrapper/gradle-wrapper.jar /gradle/wrapper/gradle-wrapper.jar
/gradle/wrapper/gradle-wrapper.properties /gradle/wrapper/gradle-wrapper.properties
/node_modules /node_modules
.kotlin
/.gigaide/gigaide.properties
/java_pid40366.hprof
+115 -34
View File
@@ -1,6 +1,29 @@
# Binary tools and BiPack serializer # Binary tools and BiPack serializer
Multiplatform binary tools collection, including portable serialization of the compact and fast [Bipack] format, 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. Multiplatform binary tools collection, including portable serialization of the compact and fast [Bipack] format, 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.
# Recent changes
- 0.1.12 published on all platforms. many small additions.
- 0.1.11 added interesting collection classes: auto-sorted list with comparator, expirable cache, etc.
- 0.1.7 built with kotlin 2.0.20 which contains important fix in wasmJS
- 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.
- 0.1.1: added serialized KVStorage with handy implementation on JVM and JS platforms and some required synchronization
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
@@ -13,15 +36,75 @@ repositories {
} }
``` ```
And add dependecy to the proper place in yuor project like this: And add dependency to the proper place in your project like this:
```kotlin ```kotlin
dependencies { dependencies {
// ... // ...
implementation("net.sergeych:mp_bintools:0.0.3") implementation("net.sergeych:mp_bintools:0.1.12")
} }
``` ```
## Calculating CRCs:
~~~kotlin
CRC.crc32("Hello".encodeToByteArray())
CRC.crc16("happy".encodeToByteArray())
CRC.crc8("world".encodeToByteArray())
~~~
## Binary effective serialization with Bipack:
~~~kotlin
@Serializable
data class Foo(val bar: String,buzz: Int)
val foo = Foo("bar", 42)
val bytes = BipackEncoder.encode(foo)
val bar: Foo = BipackDecoder.decode(bytes)
assertEquals(foo, bar)
~~~
## Bipack-based auto-serializing storage:
Allows easily storing whatever `@Serializable` data type using delegates
and more:
~~~kotlin
val storage = defaultNamedStorage("test_mp_bintools")
var foo by s1("unknown") // default value makes it a String
foo = "bar"
// nullable:
var answer: Int? by storage.optStored()
answer = 42
s1.delete("foo")
~~~
## MotherPacker
This conception allows switching encoding on the fly. Create some MotherPacker instance
and pass it to your encoding/decoding code:
~~~kotlin
@Serializable
data class FB1(val foo: Int,val bar: String)
// This is JSON implementation of MotherPacker:
val mp = JsonPacker()
// it packs and unpacks to JSON:
println(mp.pack(mapOf("foo" to 42)).decodeToString())
assertEquals("""{"foo":42}""", mp.pack(mapOf("foo" to 42)).decodeToString())
val x = mp.unpack<FB1>("""{"foo":42, "bar": "foo"}""".encodeToByteArray())
assertEquals(42, x.foo)
assertEquals("foo", x.bar)
~~~
There is also [MotherBipack] `MotherPacker` implementation using Bipack. You can add more formats
easily by implementing `MotherPacker` interface.
# Bipack # Bipack
## Why? ## Why?
@@ -30,56 +113,55 @@ Bipack is a compact and efficient binary serialization library (and format) was
### Allow easy unpacking existing binary structures ### Allow easy unpacking existing binary structures
Yuo describe your structure as `@Serializable` classes, and - voila, bipack decodes and encodes it for you! We aim to make it really easy to convert data from other binary formats by adding more format annotations Yuo describe your structure as `@Serializable` classes, and - voilà, bipack decodes and encodes it for you! We aim to make it really easy to convert data from other binary formats by adding more format annotations
### Be as compact as possible ### Be as compact as possible
For this reason it is a binary notation, it uses binary form for decimal numbers and can use variery of encoding for For this reason it is a binary notation, it uses binary form for decimal numbers and can use a variety of encoding for
integers: integers:
#### Varint #### Varint
Variable-length compact encoding is used internally in some cases. It uses a 0x80 bit in every byte to mark coninuation. Variable-length compact encoding is used internally in some cases. It uses a 0x80 bit in every byte to mark continuation.
See `object Varint`. See `object Varint`.
#### Smartint #### Smartint
Variable-length compact encoding for signed and unsigned integers use as few bytes as possible to encode integers. It is Variable-length compact encoding for signed and unsigned integers uses as few bytes as possible to encode integers. It is used automatically when serializing integers. It is slightly more sophisticated than straight `Varint`.
used automatically when serializing integers. It is slightly more sophisticated than straight `Varint`.
### Do not reveal information about stored data ### Do not reveal information about stored data
Many extendable formats, like JSON, BSON, BOSS and may others are keeping data in key-value pairs. While it is good in Many extendable formats, like JSON, BSON, BOSS and may others are keeping data in key-value pairs. While it is good in many aspects, it has some disadvantages: it uses more space, and it reveals inner data structure to the world. It is possible to unpack such formats with zero information about inner structure.
many aspets, it has a clear disadvantages: it uses more space, and it reveals inner data structure to the world. It is
possible to unpack such formats with zero information about inner structure.
Bipack does not store field names, so it is not possible to unpack or interpret it without knowledge of the data Bipack does not store field names, so it is not possible to unpack or interpret it without knowledge of the data structure. Only probabilistic analysis. Let's not make the life of attacker easier :)
structure. Only probablistic analysis. Let's not make life of attacker easier :)
### - allow upgrading data structures with backward compatibility ### -- allows upgrading data structures with backward compatibility
The dark side of serialization formats of this kind is that you can't change the structures without either loosing The serialization formats of this kind have a dark side: you can't change the structures without either losing backward compatibility with already serialized data or using voluminous boilerplate code to implement some sort of versioning.
backward compatibility with already serialzied data or using volumous boilerplate code to implement some sort of
versioning.
Not to waste space and reveal more information that needed Bipack allows extending classes marked as [@Extendable] to be Not to waste space
extended with more data _appended to the end of list of fields with required defaul values_. For such classes, Bipack stores the number of actually serialized fields and atuomatically uses default values for non-serialized ones when unpacking and reveal more information that needed Bipack allows
old data. extending classes marked
as [@Extendable] to be extended with more data _appended to the end of the field list with required default values_.
For such classes,
Bipack stores the number of actually serialized fields
and automatically uses default values for non-serialized ones when unpacking old data.
### Protect data with framing and CRC ### Protect data with framing and CRC
When needed, serialization lobrary allow to store/check CRC32 tag of the structure name with `@Framed` (can be overriden When needed,
as usual with `@SerialName`), or be followed with CRC32 of the serialized binary data, that will be checked on a serialization library allow to store/check CRC32 tag of the structure name with
deserialization, using `@CrcProtected`. This allows checking the data consistency out of the box and only where needed. `@Framed` (can be overridden as usual with `@SerialName`), or be followed with CRC32 of the serialized binary data, that will be checked on deserialization, using `@CrcProtected`. This allows checking the data consistency out of the box and only where needed.
# Usage # Usage
Use kotlinx serializatino as usual. There are the following Bipack-specific annotations at your disposal (can be combined): Use `kotlinx.serialization` as usual. There are the following Bipack-specific annotations at your disposal (can be
combined):
## @Extendable ## @Extendable
Classes marked this way store number of fields. It allows to add to the class data more fields, to the end of list, with Classes marked this way store number of fields. It allows adding to the class data more fields, to the end of the list, with
default initializers, keeping backward compatibility. For example if you have serialized: default initializers, keeping backward compatibility. For example, if you have serialized:
```kotlin ```kotlin
@Serializable @Serializable
@@ -95,28 +177,27 @@ and then decided to add a field:
data class foo(val i: Int, val bar: String = "buzz") data class foo(val i: Int, val bar: String = "buzz")
``` ```
It adds 1 or more bytes to the serialized data (field counts in `Varint` format) It adds one or more bytes to the serialized data (field counts in `Varint` format)
Bipack will properly deserialize the data serialzied for an old version. Bipack will properly deserialize the data serialized for an old version.
## @CrcProtected ## @CrcProtected
Bipack will calculate and store CRC32 of serialized data at the end, and automatically check it on deserializing Bipack will calculate and store CRC32 of serialized data at the end, and automatically check it on deserializing
throwing `InvalidFrameCRCException` if it does not match. throwing `InvalidFrameCRCException` if it does not match.
It adds 4 bytes to the serialized data. It adds four bytes to the serialized data.
## @Framed ## @Framed
Put the CRC32 of the serializing class name (`@SerialName` allows to change it as usual) and checks it on deserializing. Put the CRC32 of the serializing class name (`@SerialName` allows to change it as usual) and checks it on deserializing.
Throws `InvalidFrameHeaderException` if it does not match. Throws `InvalidFrameHeaderException` if it does not match.
It adds 4 bytes to the serialized data. It adds four bytes to the serialized data.
## @Unisgned ## @Unsigned
This __field annontation__ allows to store __integer fields__ of any size more compact by not saving the sign. Could be This __field annotation__ allows to store __integer fields__ of any size more compact by not saving the sign. It could be applied to both signed and unsigned integers of any size.
applyed to both signed and unsigned integers of any size.
## @FixedSize(size) ## @FixedSize(size)
@@ -125,7 +206,7 @@ at least one byte.
## @Fixed ## @Fixed
Can be used with any integer type to store/restor it as is, fixed-size, big-endian: Can be used with any integer type to store/restore it as is, fixed-size, big-endian:
- Short, UShort: 2 bytes - Short, UShort: 2 bytes
- Int, UInt: 4 bytes - Int, UInt: 4 bytes
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -e
./gradlew dokkaHtml
rsync -avz ./build/dokka/* code.sergeych.net:/bigstore/sergeych_pub/code/docs/mp_bintools
+65 -52
View File
@@ -1,14 +1,16 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins { plugins {
kotlin("multiplatform") version "1.8.20" kotlin("multiplatform") version "2.2.20"
kotlin("plugin.serialization") version "1.8.20" kotlin("plugin.serialization") version "2.2.20"
id("org.jetbrains.dokka") version "1.6.0" id("org.jetbrains.dokka") version "1.9.20"
`maven-publish` `maven-publish`
} }
val serialization_version = "1.3.4" val serialization_version = "1.9.0"
group = "net.sergeych" group = "net.sergeych"
version = "0.0.3" version = "0.2.2"
repositories { repositories {
mavenCentral() mavenCentral()
@@ -17,72 +19,86 @@ repositories {
} }
kotlin { kotlin {
jvm { jvmToolchain(8)
compilations.all { jvm()
kotlinOptions.jvmTarget = "1.8" js {
} browser()
withJava() nodejs()
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
js(IR) {
browser {
testTask {
useKarma {
// /home/sergeych/snap/firefox/common/.mozilla/firefox/iff469o9.default
// /home/sergeych/snap/firefox/common/.mozilla/firefox/iff469o9.default
// useFirefox()
useChromeHeadless()
// useSafari()
}
}
// commonWebpackConfig {
// cssSupport.enabled = true
// }
}
nodejs {
testTask {
}
}
} }
val hostOs = System.getProperty("os.name") macosArm64()
val isMingwX64 = hostOs.startsWith("Windows") iosX64()
val nativeTarget = when { iosArm64()
hostOs == "Mac OS X" -> macosX64("native") macosX64()
hostOs == "Linux" -> linuxX64("native") iosSimulatorArm64()
isMingwX64 -> mingwX64("native") linuxX64()
else -> throw GradleException("Host OS is not supported in Kotlin/Native.") linuxArm64()
mingwX64()
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
} }
mingwX64() {
binaries.staticLib {
baseName = "mp_bintools"
}
}
sourceSets { sourceSets {
all { all {
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi") languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
} }
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
// 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.5.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
// api("net.sergeych:mp_stools:[1.3.3,)") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") api("net.sergeych:mp_stools:[1.6.1,)")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
} }
} }
val nativeMain by creating {
dependsOn(commonMain)
dependencies {
}
}
val linuxX64Main by getting {
dependsOn(nativeMain)
}
val linuxArm64Main by getting {
dependsOn(nativeMain)
}
val commonTest by getting { val commonTest by getting {
dependencies { dependencies {
implementation(kotlin("test")) implementation(kotlin("test"))
implementation("net.sergeych:mp_stools:1.4.1") 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 jsMain by getting val jsMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-browser:0.5.0")
}
}
val jsTest by getting val jsTest by getting
val nativeMain by getting // val nativeTest by getting
val nativeTest by getting val wasmJsMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-browser:0.5.0")
}
}
val wasmJsTest by getting
}
} }
publishing { publishing {
@@ -102,9 +118,6 @@ kotlin {
} }
} }
} }
}
tasks.dokkaHtml.configure { tasks.dokkaHtml.configure {
outputDirectory.set(buildDir.resolve("dokka")) outputDirectory.set(buildDir.resolve("dokka"))
dokkaSourceSets { dokkaSourceSets {
+14 -1
View File
@@ -1,6 +1,13 @@
# Module mp_bintools # Module mp_bintools
This library contains a `Bipack` binary format serializer, see [net.sergeych.bipack.BipackEncoder] and [net.sergeych.bipack.BipackDecoder]. Also, there are many general-purpose utilities that simplify binary data processing, see package [net.sergeych.bintools] below. ## Collection of binary tools
Most interesting:
- Full set of CRC: [net.sergeych.bintools.CRC]
- Binary bit-effective __BiPack format__ serializer: [net.sergeych.bipack.BipackEncoder] and [net.sergeych.bipack.BipackDecoder]. Also typed key-value storage for it, see [net.sergeych.bipack.KVStorage] and its delegates and [net.sergeych.bipack.defaultNamedStorage].
- Multiplatform synchronization tools, that works the same and properly on JS, native and JVM, see [net.sergeych.synctools]
- many general-purpose utilities that simplify binary data processing, see package [net.sergeych.bintools] below.
# Package net.sergeych.bipack # Package net.sergeych.bipack
@@ -17,3 +24,9 @@ There are also special annotation to fine tune the format: [Extendable], [Framed
General-purpose binary tools: encoding to bytes, hex, binary dumps. variable length integer, ect. Most of it is used internally by bipack serializers, see [net.sergeych.bipack] for details. General-purpose binary tools: encoding to bytes, hex, binary dumps. variable length integer, ect. Most of it is used internally by bipack serializers, see [net.sergeych.bipack] for details.
In particular, see [Varint] and [Smartint] variable-length compact integer codecs and also [DataSource] and [DataSink] multiplatform synchronous read/write interfaces. In particular, see [Varint] and [Smartint] variable-length compact integer codecs and also [DataSource] and [DataSink] multiplatform synchronous read/write interfaces.
# Package net.sergeych.synctools
To write a code that compiles and runs, and most likely works on the
JS, native, and JVM, we need some portable/compatible synchronization
primitives. This package is a collection of such.
+2 -1
View File
@@ -1,2 +1,3 @@
kotlin.code.style=official kotlin.code.style=official
kotlin.js.compiler=ir kotlin.mpp.applyDefaultHierarchyTemplate=false
kotlin.daemon.jvmargs=-Xmx3072M
+644 -461
View File
File diff suppressed because it is too large. Load diff
@@ -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)
@@ -27,6 +27,9 @@ interface CRC<T> {
fun crc8(data: ByteArray, polynomial: UByte = 0xA7.toUByte()): UByte = fun crc8(data: ByteArray, polynomial: UByte = 0xA7.toUByte()): UByte =
CRC8(polynomial).also { it.update(data) }.value CRC8(polynomial).also { it.update(data) }.value
fun crc8(data: UByteArray, polynomial: UByte = 0xA7.toUByte()): UByte =
CRC8(polynomial).also { it.update(data) }.value
/** /**
* Calculate CRC16 for a data array using a given polynomial (CRC16-CCITT polynomial (0x1021) by default) * Calculate CRC16 for a data array using a given polynomial (CRC16-CCITT polynomial (0x1021) by default)
*/ */
@@ -45,6 +48,7 @@ infix fun UShort.shl(bitCount: Int): UShort = (this.toUInt() shl bitCount).toUSh
infix fun UShort.shr(bitCount: Int): UShort = (this.toUInt() shr bitCount).toUShort() infix fun UShort.shr(bitCount: Int): UShort = (this.toUInt() shr bitCount).toUShort()
infix fun UByte.shl(bitCount: Int): UByte = (this.toUInt() shl bitCount).toUByte() infix fun UByte.shl(bitCount: Int): UByte = (this.toUInt() shl bitCount).toUByte()
@Suppress("unused")
infix fun UByte.shr(bitCount: Int): UByte = (this.toUInt() shr bitCount).toUByte() infix fun UByte.shr(bitCount: Int): UByte = (this.toUInt() shr bitCount).toUByte()
fun UByte.toBigEndianUShort(): UShort = this.toUShort() shl 8 fun UByte.toBigEndianUShort(): UShort = this.toUShort() shl 8
@@ -0,0 +1,135 @@
package net.sergeych.bintools
import net.sergeych.bipack.BipackDecoder
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.WaitHandle
import net.sergeych.synctools.withLock
private val ac = AtomicCounter(0)
class DataKVStorage(private val provider: DataProvider) : KVStorage,
Loggable by LogTag("DKVS${ac.incrementAndGet()}") {
data class Lock(val name: String) {
private val exclusive = ProtectedOp()
private var readerCount = 0
private var pulses = WaitHandle()
fun <T> lockExclusive(f: () -> T): T {
while (true) {
exclusive.withLock {
if (readerCount == 0) {
return f()
}
}
pulses.await()
}
}
fun <T> lockRead(f: () -> T): T {
try {
exclusive.withLock { readerCount++ }
return f()
} finally {
exclusive.withLock { readerCount-- }
if (readerCount == 0) pulses.wakeUp()
}
}
}
private val locks = mutableMapOf<String, Lock>()
private val access = ProtectedOp()
private val keyIds = mutableMapOf<String, Int>()
private var lastId: Int = 0
init {
access.withLock {
// TODO: read keys
for (fn in provider.list()) {
debug { "Scanning: $fn" }
if (fn.endsWith(".d")) {
val id = fn.dropLast(2).toInt(16)
debug { "found data record: $fn -> $id" }
val name = provider.read(fn) { BipackDecoder.decode<String>(it) }
keyIds[name] = id
if (id > lastId) lastId = id
} else debug { "ignoring record $fn" }
}
}
}
/**
* Important: __it must be called with locked access op!__
*/
private fun lockFor(name: String): Lock = locks.getOrPut(name) { Lock(name) }
private fun recordName(id: Int) = "${id.toString(16)}.d"
private fun <T> read(name: String, f: (DataSource) -> T): T {
val lock: Lock
val id: Int
// global lock: fast
access.withLock {
id = keyIds[name] ?: throw DataProvider.NotFoundException()
lock = lockFor(name)
}
// per-name lock: slow
return lock.lockRead {
provider.read(recordName(id), f)
}
}
private fun write(name: String, f: (DataSink) -> Unit) {
val lock: Lock
val id: Int
// global lock: fast
access.withLock {
id = keyIds[name] ?: (++lastId).also { keyIds[name] = it }
lock = lockFor(name)
}
// per-name lock: slow
lock.lockExclusive { provider.write(recordName(id), f) }
}
private fun deleteEntry(name: String) {
// fast pre-check:
if (name !in keyIds) return
// global lock: we can't now detect concurrent delete + write ops, so exclusive:
access.withLock {
val id = keyIds[name] ?: return
provider.delete(recordName(id))
locks.remove(name)
keyIds.remove(name)
}
}
override fun get(key: String): ByteArray? = try {
read(key) {
BipackDecoder.decode<String>(it)
// notice: not nullable byte array here!
BipackDecoder.decode<ByteArray>(it)
}
} catch (_: DataProvider.NotFoundException) {
null
}
override fun set(key: String, value: ByteArray?) {
if (value == null) {
deleteEntry(key)
} else write(key) {
BipackEncoder.encode(key, it)
BipackEncoder.encode(value, it)
}
}
override val keys: Set<String>
get() = access.withLock { keyIds.keys }
}
@@ -0,0 +1,33 @@
package net.sergeych.bintools
/**
* Abstraction of some file- or named_record- storage. It could be a filesystem
* on native and JVM targets and indexed DB or session storage in the browser.
*/
interface DataProvider {
open class Error(msg: String,cause: Throwable?=null): Exception(msg,cause)
class NotFoundException(msg: String="record not found",cause: Throwable? = null): Error(msg, cause)
class WriteFailedException(msg: String="can't write", cause: Throwable?=null): Error(msg,cause)
/**
* Read named record/file
* @throws NotFoundException
*/
fun <T>read(name: String,f: (DataSource)->T): T
/**
* Write to a named record / file.
* @throws WriteFailedException
*/
fun write(name: String,f: (DataSink)->Unit)
/**
* Delete if exists, or do nothing.
*/
fun delete(name: String)
/**
* List all record names in this source
*/
fun list(): List<String>
}
@@ -1,5 +1,6 @@
package net.sergeych.bintools package net.sergeych.bintools
@Suppress("unused")
interface DataSink { interface DataSink {
fun writeByte(data: Byte) fun writeByte(data: Byte)
@@ -24,9 +25,9 @@ interface DataSink {
} }
fun writeVarUInt(value: UInt) { Varint.encodeUnsigned(value.toULong(), this)} fun writeVarUInt(value: UInt) { Varint.encodeUnsigned(value.toULong(), this)}
fun writeVarInt(value: UInt) { Varint.encodeSigned(value.toLong(), this)} fun writeVarInt(value: Int) { Varint.encodeSigned(value.toLong(), this)}
fun writeSmartUInt(value: UInt) { Smartint.encodeUnsigned(value.toULong(), this)} fun writeSmartUInt(value: UInt) { Smartint.encodeUnsigned(value.toULong(), this)}
fun writeSmartInt(value: UInt) { Smartint.encodeSigned(value.toLong(), this)} fun writeSmartInt(value: Int) { Smartint.encodeSigned(value.toLong(), this)}
} }
inline fun <reified T:Any>DataSink.writeNumber(value: T) { inline fun <reified T:Any>DataSink.writeNumber(value: T) {
@@ -6,6 +6,7 @@ package net.sergeych.bintools
* like multiplatform version of DataInput * like multiplatform version of DataInput
* *
*/ */
@Suppress("unused")
interface DataSource { interface DataSource {
/** /**
@@ -15,6 +16,12 @@ interface DataSource {
fun readByte(): Byte fun readByte(): Byte
/**
* true if there is no more data available and next read operation will surely
* throw EndOfData. Can return null if it is impossible to determine (for some
* async sources)
*/
fun isEnd(): Boolean? = null
fun readUByte() = readByte().toUByte() fun readUByte() = readByte().toUByte()
@@ -33,7 +40,7 @@ interface DataSource {
fun readDouble() = Double.fromBits(readI64()) fun readDouble() = Double.fromBits(readI64())
fun readFloat() = Float.fromBits(readI32()).toFloat() fun readFloat() = Float.fromBits(readI32())
fun readSmartUInt(): UInt = Smartint.decodeUnsigned(this).toUInt() fun readSmartUInt(): UInt = Smartint.decodeUnsigned(this).toUInt()
fun readSmartInt(): Int = Smartint.decodeSigned(this).toInt() fun readSmartInt(): Int = Smartint.decodeSigned(this).toInt()
@@ -44,12 +51,14 @@ interface DataSource {
} }
fun ByteArray.toDataSource(): DataSource = fun ByteArray.toDataSource(): DataSource =
object : DataSource { object : DataSource {
var position = 0 var position = 0
private set private set
@Suppress("RedundantNullableReturnType")
override fun isEnd(): Boolean? = position == size
override fun readByte(): Byte = override fun readByte(): Byte =
if (position < size) this@toDataSource[position++] if (position < size) this@toDataSource[position++]
else throw DataSource.EndOfData() else throw DataSource.EndOfData()
@@ -59,5 +68,23 @@ fun ByteArray.toDataSource(): DataSource =
} }
} }
@Suppress("unused")
fun UByteArray.toDataSource(): DataSource =
object : DataSource {
var position = 0
private set
@Suppress("RedundantNullableReturnType")
override fun isEnd(): Boolean? = position == size
override fun readByte(): Byte =
if (position < size) this@toDataSource[position++].toByte()
else throw DataSource.EndOfData()
override fun toString(): String {
return "ASrc[$position]: ${encodeToHex()}"
}
}
inline fun <reified T : Any> DataSource.readNumber(): T = Smartint.decode(this) as T inline fun <reified T : Any> DataSource.readNumber(): T = Smartint.decode(this) as T
@@ -0,0 +1,226 @@
package net.sergeych.bintools
import kotlinx.serialization.KSerializer
import kotlinx.serialization.serializer
import net.sergeych.bipack.BipackDecoder
import net.sergeych.bipack.BipackEncoder
import kotlin.reflect.KProperty
/**
* Generic storage of binary content. PArsec uses boss encoding to store everything in it
* in a convenient way. See [KVStorage.stored], [KVStorage.invoke] and
* [KVStorage.optStored] delegates. The [MemoryKVStorage] is an implementation that stores
* values in memory, allowing to connect some other (e.g. persistent storage) later in a
* completely transparent way. It can also be used to cache values on the fly.
*
* Also, it is possible to use [read] and [write] where delegated properties
* do not fit well.
*/
@Suppress("unused")
interface KVStorage {
operator fun get(key: String): ByteArray?
operator fun set(key: String, value: ByteArray?)
/**
* Check whether key is in storage.
* Default implementation uses [keys]. You may override it for performance
*/
operator fun contains(key: String): Boolean = key in keys
val keys: Set<String>
/**
* Get number of object in the storage
* Default implementation uses [keys]. You may override it for performance
*/
val size: Int get() = keys.size
/**
* Clears all objects in the storage
* Default implementation uses [keys]. You may override it for performance
*/
fun clear() {
for (k in keys.toList()) this[k] = null
}
/**
* Default implementation uses [keys]. You may override it for performance
*/
fun isEmpty() = size == 0
/**
* Default implementation uses [keys]. You may override it for performance
*/
fun isNotEmpty() = size != 0
/**
* Add all elements from another storage, overwriting any existing
* keys.
*/
fun addAll(other: KVStorage) {
for (k in other.keys) {
this[k] = other[k]
}
}
/**
* Delete element by key
*/
fun delete(key: String) {
set(key, null)
}
}
/**
* Write
*/
inline fun <reified T>KVStorage.write(key: String,value: T) {
this[key] = BipackEncoder.encode<T>(value)
}
@Suppress("unused")
inline fun <reified T>KVStorage.save(key: String,value: T?) {
if( value != null ) write(key, value)
else delete(key)
}
inline fun <reified T:Any>KVStorage.read(key: String): T? =
this[key]?.let { BipackDecoder.decode<T>(it) }
@Suppress("unused")
inline fun <reified T:Any>KVStorage.load(key: String): T? = read(key)
inline operator fun <reified T: Any> KVStorage.invoke(defaultValue: T,overrideName: String? = null) =
KVStorageDelegate(this, serializer<T>(), defaultValue, overrideName)
inline fun <reified T: Any> KVStorage.stored(defaultValue: T, overrideName: String? = null) =
KVStorageDelegate(this, serializer<T>(), defaultValue, overrideName)
inline fun <reified T: Any> KVStorage.optStored(overrideName: String? = null) =
KVStorageOptDelegate<T>(this, serializer<T>(),overrideName)
class KVStorageDelegate<T: Any>(
private val storage: KVStorage,
private val serializer: KSerializer<T>,
private val defaultValue: 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) }
?: defaultValue
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
storage[name(property)] = BipackEncoder.encode(serializer, value)
}
}
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)
}
}
/**
* Memory storage. Allows connecting to another storage (e.g. persistent one) at come
* point later in a transparent way.
*/
class MemoryKVStorage(copyFrom: KVStorage? = null) : KVStorage {
// is used when connected:
private var underlying: KVStorage? = null
// is used while underlying is null:
private val data = mutableMapOf<String, ByteArray>()
/**
* Connect some other storage. All existing data will be copied to the [other]
* storage. After this call all data access will be routed to [other] storage.
*/
@Suppress("unused")
fun connectToStorage(other: KVStorage) {
other.addAll(this)
underlying = other
data.clear()
}
/**
* Get data from either memory or a connected storage, see [connectToStorage]
*/
override fun get(key: String): ByteArray? {
underlying?.let {
return it[key]
}
return data[key]
}
/**
* Put data to memory storage or connected storage if [connectToStorage] was called
*/
override fun set(key: String, value: ByteArray?) {
underlying?.let { it[key] = value } ?: run {
if (value != null) data[key] = value
else data.remove(key)
}
}
/**
* Checks the item exists in the memory storage or connected one, see[connectToStorage]
*/
override fun contains(key: String): Boolean {
underlying?.let { return key in it }
return key in data
}
override val keys: Set<String>
get() = underlying?.keys ?: data.keys
override fun clear() {
underlying?.clear() ?: data.clear()
}
init {
copyFrom?.let { addAll(it) }
}
}
/**
* Create per-platform default named storage.
*
* - In the browser, it uses the `Window.localStorage` prefixing items
* by a string containing the [name]
*
* - In the JVM environment it uses folder-based storage on the file system. The name
* is considered to be a folder name (the whole path which will be automatically created)
* using the following rules:
* - when the name starts with slash (`/`) it is treated as an absolute path to a folder
* - when the name contains slash, it is considered to be a relative folder to the
* `User.home` directory, like "`~/`" on unix systems.
* - otherwise, the folder will be created in "`~/.local_storage`" parent directory
* (which also will be created if needed).
*
* - For the native platorms it is not yet implemented (but will be soon).
*
* See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like,
* and `FileDataProvider` class on JVM target.
*/
expect fun defaultNamedStorage(name: String): KVStorage
@@ -0,0 +1,65 @@
package net.sergeych.bintools
/**
* 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].
*/
@Deprecated("moved to net.sergeych.collections", ReplaceWith("net.sergeych.collections.MRUCache"))
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()
}
@@ -24,6 +24,7 @@ class JsonPacker : MotherPacker {
return Json.encodeToString(serializer(type), payload).encodeToByteArray() return Json.encodeToString(serializer(type), payload).encodeToByteArray()
} }
@Suppress("UNCHECKED_CAST")
override fun <T> unpack(type: KType, packed: ByteArray): T { override fun <T> unpack(type: KType, packed: ByteArray): T {
return Json.decodeFromString<T>( return Json.decodeFromString<T>(
serializer(type) as KSerializer<T>, serializer(type) as KSerializer<T>,
@@ -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
} }
@@ -106,12 +104,15 @@ fun UByte.encodeToHex(length: Int = 0) = toLong().encodeToHex(length)
fun ULong.encodeToHex(length: Int = 0) = toLong().encodeToHex(length) fun ULong.encodeToHex(length: Int = 0) = toLong().encodeToHex(length)
fun ByteArray.encodeToHex(separator: String = " "): String = joinToString(separator) { it.toUByte().encodeToHex(2) } fun ByteArray.encodeToHex(separator: String = " "): String = joinToString(separator) { it.toUByte().encodeToHex(2) }
fun UByteArray.encodeToHex(separator: String = " "): String = joinToString(separator) { it.encodeToHex(2) }
@Suppress("unused") @Suppress("unused")
fun Collection<Byte>.encodeToHex(separator: String = " "): String = joinToString(separator) { it.toUByte().encodeToHex(2) } fun Collection<Byte>.encodeToHex(separator: String = " "): String = joinToString(separator) { it.toUByte().encodeToHex(2) }
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
@@ -3,6 +3,7 @@ package net.sergeych.bipack
import kotlinx.datetime.Instant 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.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.encoding.AbstractDecoder import kotlinx.serialization.encoding.AbstractDecoder
@@ -27,12 +28,16 @@ class BipackDecoder(
private var fixedSize = -1 private var fixedSize = -1
private var fixedNumber = false private var fixedNumber = false
override val serializersModule: SerializersModule = EmptySerializersModule override val serializersModule: SerializersModule = EmptySerializersModule()
override fun decodeBoolean(): Boolean = input.readByte().toInt() != 0 override fun decodeBoolean(): Boolean = input.readByte().toInt() != 0
override fun decodeByte(): Byte = input.readByte() override fun decodeByte(): Byte = input.readByte()
override fun decodeShort(): Short = override fun decodeShort(): Short =
if (fixedNumber) input.readI16() if (fixedNumber) input.readI16()
else if (nextIsUnsigned) input.readNumber<UInt>().toShort() else input.readNumber() else if (nextIsUnsigned)
input.readNumber<UInt>().toShort()
else
input.readNumber()
override fun decodeInt(): Int = override fun decodeInt(): Int =
if (fixedNumber) input.readI32() if (fixedNumber) input.readI32()
else if (nextIsUnsigned) input.readNumber<UInt>().toInt() else input.readNumber() else if (nextIsUnsigned) input.readNumber<UInt>().toInt() else input.readNumber()
@@ -40,6 +45,7 @@ class BipackDecoder(
override fun decodeLong(): Long = override fun decodeLong(): Long =
if (fixedNumber) input.readI64() if (fixedNumber) input.readI64()
else if (nextIsUnsigned) input.readNumber<ULong>().toLong() else input.readNumber() else if (nextIsUnsigned) input.readNumber<ULong>().toLong() else input.readNumber()
override fun decodeFloat(): Float = input.readFloat() override fun decodeFloat(): Float = input.readFloat()
override fun decodeDouble(): Double = input.readDouble() override fun decodeDouble(): Double = input.readDouble()
override fun decodeChar(): Char = Char(input.readNumber<UInt>().toInt()) override fun decodeChar(): Char = Char(input.readNumber<UInt>().toInt())
@@ -54,7 +60,8 @@ class BipackDecoder(
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = input.readNumber<UInt>().toInt() override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = input.readNumber<UInt>().toInt()
override fun decodeElementIndex(descriptor: SerialDescriptor): Int { override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
if (elementIndex >= elementsCount) return CompositeDecoder.DECODE_DONE if (elementIndex >= elementsCount)
return CompositeDecoder.DECODE_DONE
nextIsUnsigned = false nextIsUnsigned = false
for (a in descriptor.getElementAnnotations(elementIndex)) { for (a in descriptor.getElementAnnotations(elementIndex)) {
when (a) { when (a) {
@@ -72,6 +79,8 @@ class BipackDecoder(
else else
super.decodeSerializableValue(deserializer) super.decodeSerializableValue(deserializer)
} }
override fun decodeSequentially(): Boolean = isCollection override fun decodeSequentially(): Boolean = isCollection
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
@@ -118,7 +127,11 @@ class BipackDecoder(
super.endStructure(descriptor) super.endStructure(descriptor)
} }
override fun decodeNotNullMark(): Boolean = decodeBoolean() override fun decodeNotNullMark(): Boolean = try {
decodeBoolean()
} catch (_: DataSource.EndOfData) {
false
}
@ExperimentalSerializationApi @ExperimentalSerializationApi
override fun decodeNull(): Nothing? = null override fun decodeNull(): Nothing? = null
@@ -131,7 +144,16 @@ class BipackDecoder(
inline fun <reified T> decode(source: DataSource): T = decode(source, serializer()) inline fun <reified T> decode(source: DataSource): T = decode(source, serializer())
inline fun <reified T> decode(source: ByteArray): T = inline fun <reified T> decode(source: ByteArray): T =
decode(source.toDataSource(), serializer()) decode(source.toDataSource(), serializer())
fun <T> decode(serializer: KSerializer<T>, source: ByteArray): T =
decode(source.toDataSource(), serializer)
inline fun <reified T> decode(source: UByteArray): T =
decode(source.toDataSource(), serializer())
fun <T> decode(serializer: KSerializer<T>, source: UByteArray): T =
decode(source.toDataSource(), serializer)
} }
} }
inline fun <reified T> ByteArray.decodeFromBipack() = BipackDecoder.decode<T>(this) inline fun <reified T> ByteArray.decodeFromBipack() = BipackDecoder.decode<T>(this)
@Suppress("unused")
inline fun <reified T> UByteArray.decodeFromBipack() = BipackDecoder.decode<T>(this)
@@ -28,15 +28,15 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
} }
} }
override val serializersModule: SerializersModule = EmptySerializersModule override val serializersModule: SerializersModule = EmptySerializersModule()
override fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0) override fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0)
override fun encodeByte(value: Byte) = output.writeByte(value.toInt()) override fun encodeByte(value: Byte) = output.writeByte(value.toInt())
override fun encodeShort(value: Short) = override fun encodeShort(value: Short) =
if (fixedNumber) output.writeI16(value) if (fixedNumber) output.writeI16(value)
else if (nextIsUnsigned) else if (nextIsUnsigned)
output.writeNumber(value.toUInt()) output.writeNumber(value.toUShort())
else else
output.writeNumber(value.toInt()) output.writeNumber(value)
override fun encodeInt(value: Int) = override fun encodeInt(value: Int) =
if (fixedNumber) if (fixedNumber)
@@ -126,3 +126,8 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
} }
} }
@Suppress("unused")
inline fun <reified T> toBipackUByteArray(value: T): UByteArray = BipackEncoder.encode(value).toUByteArray()
@Suppress("unused")
inline fun <reified T> toBipackByteArray(value: T): ByteArray = BipackEncoder.encode(value)
@@ -6,11 +6,13 @@ import net.sergeych.bintools.MotherPacker
import net.sergeych.bintools.toDataSource import net.sergeych.bintools.toDataSource
import kotlin.reflect.KType import kotlin.reflect.KType
@Suppress("unused")
class MotherBipack : MotherPacker { class MotherBipack : MotherPacker {
override fun <T> pack(type: KType, payload: T): ByteArray { override fun <T> pack(type: KType, payload: T): ByteArray {
return BipackEncoder.encode(serializer(type), payload) return BipackEncoder.encode(serializer(type), payload)
} }
@Suppress("UNCHECKED_CAST")
override fun <T> unpack(type: KType, packed: ByteArray): T { override fun <T> unpack(type: KType, packed: ByteArray): T {
return BipackDecoder.decode<T>(packed.toDataSource(), return BipackDecoder.decode<T>(packed.toDataSource(),
serializer(type) as KSerializer<T>) serializer(type) as KSerializer<T>)
@@ -3,15 +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 leading number of fields. This allows to extend 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__.
* *
* Whe deserializing such instances from previous version binaries, the new parameters * This annotation makes Bipack to insert fields count before the
* will get default values. * serialized data. It then checks it on deserializing to fill not serialized fields will
* default values.
* *
* Serialized data of classes not market as ExtendableFormat could not be changed without * Note that since 0.0.7 the same behavior could be achieved by serializing each instance in the
* breaking compatibility with existing serialized data. * 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.
*
* _Please note that without this annotation it could be impossible to deserialize old versions of
* the class, in particular, in array, inner fields, etc._
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@SerialInfo @SerialInfo
@@ -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()) }
@@ -0,0 +1,168 @@
package net.sergeych.collections
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import net.sergeych.mptools.withReentrantLock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/**
* 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
}
@@ -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()
}
@@ -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) }
}
}
@@ -0,0 +1,15 @@
package net.sergeych.synctools
/**
* Thread-safe multiplatform counter
*/
@Suppress("unused")
class AtomicCounter(initialValue: Long = 0) : AtomicValue<Long>(initialValue) {
fun incrementAndGet(): Long = op { ++actualValue }
fun getAndIncrement(): Long = op { actualValue++ }
fun decrementAndGet(): Long = op { --actualValue }
fun getAndDecrement(): Long = op { actualValue-- }
}
@@ -0,0 +1,37 @@
package net.sergeych.synctools
/**
* Multiplatform (JS and battery included) atomically mutable value.
* Actual value can be either changed in a block of [mutate] when
* new value _depends on the current value_ or use a same [value]
* property that is thread-safe where there are threads and just safe
* otherwise ;)
*/
open class AtomicValue<T>(initialValue: T) {
var actualValue = initialValue
protected set
protected val op = ProtectedOp()
/**
* Change the value: get the current and set to the returned, all in the
* atomic operation. All other mutating requests including assigning to [value]
* will be blocked and queued.
* @return result of the mutation. Note that immediate call to property [value]
* could already return modified bu some other thread value!
*/
fun mutate(mutator: (T) -> T): T = op {
actualValue = mutator(actualValue)
actualValue
}
/**
* Atomic get or set the value. Atomic get means if there is a [mutate] in progress
* it will wait until the mutation finishes and then return the correct result.
*/
var value: T
get() = op { actualValue }
set(value) {
mutate { value }
}
}
@@ -0,0 +1,60 @@
package net.sergeych.synctools
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Multiplatform interface to perform a regular (not suspend) operation
* protected by a platform mutex (where necessary). Get real implementation
* with [ProtectedOp] and use it with [ProtectedOpImplementation.withLock] and
* [ProtectedOpImplementation.invoke]
*/
interface ProtectedOpImplementation {
/**
* Get a lock. Be sure to release it.
* The recommended way is using [ProtectedOpImplementation.withLock] and
* [ProtectedOpImplementation.invoke]
*/
fun lock()
/**
* Release a lock.
* The recommended way is using [ProtectedOpImplementation.withLock] and
* [ProtectedOpImplementation.invoke]
*/
fun unlock()
}
@ExperimentalContracts
inline fun <T> ProtectedOpImplementation.withLock(f: () -> T): T {
contract {
callsInPlace(f, InvocationKind.EXACTLY_ONCE)
}
lock()
return try {
f()
} finally {
unlock()
}
}
/**
* Run a block mutualy-exclusively, see [ProtectedOp]
*/
operator fun <T> ProtectedOpImplementation.invoke(f: () -> T): T = withLock(f)
/**
* Get the platform-depended implementation of a mutex. It does nothing in the
* browser and use appropriate mechanics on JVM and native targets. See
* [ProtectedOpImplementation.invoke], [ProtectedOpImplementation.withLock]
* ```kotlin
* val op = ProtectedOp()
* //...
* op {
* // mutually exclusive execution
* println("sequential execution here")
* }
* ~~~
*/
expect fun ProtectedOp(): ProtectedOpImplementation
@@ -0,0 +1,23 @@
package net.sergeych.synctools
/**
* Platform-independent interface to thread wait/notify. Does nothing in JS/browser,
* and uses appropriate mechanics on other platforms.
*/
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
expect class WaitHandle() {
/**
* Wait for [wakeUp] as long as [milliseconds] milliseconds, or forever.
* Notice it returns immediately on the single-theaded platforms like JS.
* @param milliseconds to wait, use 0 to wait indefinitely
* @return true if [wakeUp] was called before the timeout, false on timeout.
*/
fun await(milliseconds: Long=0): Boolean
/**
* Awake all [await]'ing threads. Does nothing in the single-threaded JS
* environment
*/
fun wakeUp()
}
@@ -0,0 +1,24 @@
package net.sergecyh.diwan.tools
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlin.time.Duration
/**
* 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
}
@@ -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() }
}
}
@@ -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
}
}
@@ -0,0 +1,27 @@
package net.sergecyh.diwan.tools
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlin.time.Duration
/**
* 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
}
+16
View File
@@ -0,0 +1,16 @@
package bintools
import net.sergeych.bintools.CRC
import net.sergeych.bintools.encodeToHex
import kotlin.test.Test
class CrcTest {
@Test
fun testVectors() {
val x = byteArrayOf(1,2,3,4,5)
val crc = CRC.crc8(x)
println("->> ${x.toList()}")
println("->> ${crc.encodeToHex()}")
println("->> ${crc}")
}
}
@@ -65,4 +65,14 @@ class SmartintTest {
} }
} }
@Test
fun testCustom() {
val x = 64000.toULong()
val p = Smartint.encodeUnsigned(x)
println(p.toDump())
val y = Smartint.decodeUnsigned(p)
println(y)
assertEquals(y, x)
}
} }
@@ -1,7 +1,11 @@
package bintools package bintools
import net.sergeych.bintools.MRUCache
import net.sergeych.bintools.toDump import net.sergeych.bintools.toDump
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class TestTools { class TestTools {
@Test @Test
@@ -16,4 +20,42 @@ class TestTools {
println(res.toDump()) println(res.toDump())
} }
} }
@Test
fun testCache() {
val cache = MRUCache<Int,String>(3)
cache.putAll( mapOf(1 to "one", 2 to "two", 3 to "three", 4 to "four" ) )
assertNull(cache[0])
// this actually should reset MRU for 2:
assertEquals("two", cache[2])
assertNull(cache[1])
assertEquals(3, cache.size)
assertTrue { 3 in cache }
assertTrue { 4 in cache }
// now MRU is 2 (checked in assertEquals above) so LRU to drop is 3!
cache[5] = "five"
assertEquals(3, cache.size)
assertTrue { 2 in cache }
assertTrue { 4 in cache }
assertTrue { 5 in cache }
cache.getOrPut(3) { "new three"}
assertEquals(3, cache.size)
assertTrue { 2 in cache }
assertTrue { 3 in cache }
assertTrue { 5 in cache }
cache[2] = "New Two"
cache[6] = "six"
// 2 is now second used
// amd 6 is MRU, oldest is therefore 5
assertEquals(3, cache.size)
assertTrue { 2 in cache }
assertTrue { 6 in cache }
assertTrue { 3 in cache }
}
} }
@@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
import net.sergeych.bintools.encodeToHex import net.sergeych.bintools.encodeToHex
import net.sergeych.bintools.toDump import net.sergeych.bintools.toDump
import net.sergeych.bipack.* import net.sergeych.bipack.*
import net.sergeych.mp_tools.encodeToBase64Compact
import kotlin.experimental.xor import kotlin.experimental.xor
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContentEquals import kotlin.test.assertContentEquals
@@ -29,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")
@@ -45,6 +47,14 @@ data class FoobarF3(val bar: Int, val foo: Int, val other: Int = -1)
@CrcProtected() @CrcProtected()
data class FoobarFP1(val bar: Int, val foo: Int, val other: Int = -1) data class FoobarFP1(val bar: Int, val foo: Int, val other: Int = -1)
@Serializable
sealed class SC1 {
@Serializable
class Nested : SC1()
}
@Suppress("unused")
class BipackEncoderTest { class BipackEncoderTest {
@Serializable @Serializable
@@ -314,6 +324,7 @@ class BipackEncoderTest {
@Serializable @Serializable
data class FU16(@Fixed val i: UShort) data class FU16(@Fixed val i: UShort)
@Serializable @Serializable
class Foo( class Foo(
@Fixed @Fixed
@@ -347,8 +358,104 @@ class BipackEncoderTest {
@Test @Test
fun testInstant() { fun testInstant() {
val x = Clock.System.now() val x = Clock.System.now()
// println( BipackEncoder.encode(x).toDump() ) println(BipackEncoder.encode(x).toDump())
val y = BipackDecoder.decode<Instant>(BipackEncoder.encode(x)) val y = BipackDecoder.decode<Instant>(BipackEncoder.encode(x))
assertEquals(x.toEpochMilliseconds(), y.toEpochMilliseconds()) assertEquals(x.toEpochMilliseconds(), y.toEpochMilliseconds())
} }
@Serializable
data class UInts(
val b: UByte,
@Fixed
val si: UShort,
@Fixed
val i: UInt,
@Fixed
val li: ULong,
)
@Serializable
data class VarUInts(
val b: UByte,
@Unsigned
val si: UShort,
@Unsigned
val i: UInt,
@Unsigned
val li: ULong,
)
@Test
fun vectors() {
val x = UInts(7u, 64000.toUShort(), 66000u, 931127140399u)
val p = BipackEncoder.encode(x)
println(p.toDump())
println(p.encodeToBase64Compact())
val y = BipackDecoder.decode<UInts>(p)
assertEquals(x, y)
val xv = VarUInts(7u, 64000.toUShort(), 66000u, 931127140399u)
val pv = BipackEncoder.encode(xv)
println(pv.toDump())
println(pv.encodeToBase64Compact())
val yv = BipackDecoder.decode<VarUInts>(pv)
assertEquals(xv, yv)
println(xv)
println(yv)
}
@Test
fun testStrangeUnpack() {
@Serializable
data class SFoo(val code: Int, val s1: String? = null, val s2: String? = null)
val z = BipackEncoder.encode(117)
println(z.toDump())
val sf = BipackDecoder.decode<SFoo>(z)
println(sf)
}
@Serializable
enum class TU1 {
N1, N2, N3, N4
}
@Test
fun testUnsignedEnums() {
val p1 = BipackEncoder.encode(TU1.N4)
// val p2 = BipackEncoder.encode(3u)
println(p1.toDump())
// println(p2.toDump())
val t2 = BipackDecoder.decode<TU1>(p1)
assertEquals(TU1.N4, t2)
assertEquals(0x0cu, p1[0].toUByte())
}
@Test
fun testClosedSerialization() {
val x: SC1 = SC1.Nested()
val b = BipackEncoder.encode(x)
println(b.toDump())
val y = BipackDecoder.decode<SC1>(b)
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)
}
} }
@@ -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)
}
}
@@ -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() }
}
}
@@ -0,0 +1,20 @@
package net.sergeych.synctools
import kotlin.test.Test
import kotlin.test.assertEquals
class AtomicCounterTest {
@Test
fun incrementAndDecrement() {
val ac = AtomicCounter(7)
assertEquals(7, ac.getAndIncrement())
assertEquals(8, ac.value)
assertEquals(9, ac.incrementAndGet())
assertEquals(9, ac.value)
assertEquals(9, ac.getAndDecrement())
assertEquals(8, ac.value)
assertEquals(7, ac.decrementAndGet())
assertEquals(7, ac.value)
}
}
@@ -0,0 +1,25 @@
package net.sergeych.bintools
/**
* Create per-platform default named storage.
*
* - In the browser, it uses the `Window.localStorage` prefixing items
* by a string containing the [name]
*
* - In the JVM environment it uses folder-based storage on the file system. The name
* is considered to be a folder name (the whole path which will be automatically created)
* using the following rules:
* - when the name starts with slash (`/`) it is treated as an absolute path to a folder
* - when the name contains slash, it is considered to be a relative folder to the
* `User.home` directory, like "`~/`" on unix systems.
* - otherwise, the folder will be created in "`~/.local_storage`" parent directory
* (which also will be created if needed).
*
* - For the native platorms it is not yet implemented (but will be soon).
*
* See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like,
* and `FileDataProvider` class on JVM target.
*/
actual fun defaultNamedStorage(name: String): KVStorage {
TODO("Not yet implemented")
}
@@ -0,0 +1,27 @@
package net.sergeych.synctools
import kotlinx.atomicfu.locks.ReentrantLock
/**
* Get the platform-depended implementation of a mutex. It does nothing in the
* browser and use appropriate mechanics on JVM and native targets. See
* [ProtectedOpImplementation.invoke], [ProtectedOpImplementation.withLock]
* ```kotlin
* val op = ProtectedOp()
* //...
* op {
* // mutually exclusive execution
* println("sequential execution here")
* }
* ~~~
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val access = ReentrantLock()
override fun lock() {
access.lock()
}
override fun unlock() {
access.unlock()
}
}
@@ -0,0 +1,38 @@
package net.sergeych.synctools
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
/**
* Platform-independent interface to thread wait/notify. Does nothing in JS/browser,
* and uses appropriate mechanics on other platforms.
*/
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
private val channel = Channel<Unit>()
actual fun await(milliseconds: Long): Boolean {
return runBlocking {
try {
if( milliseconds > 0) {
withTimeout(milliseconds) {
channel.receive()
true
}
}
else {
channel.receive()
true
}
}
catch(_: TimeoutCancellationException) {
false
}
}
}
actual fun wakeUp() {
runBlocking { channel.send(Unit) }
}
}
@@ -0,0 +1,25 @@
package net.sergeych.bintools
/**
* Create per-platform default named storage.
*
* - In the browser, it uses the `Window.localStorage` prefixing items
* by a string containing the [name]
*
* - In the JVM environment it uses folder-based storage on the file system. The name
* is considered to be a folder name (the whole path which will be automatically created)
* using the following rules:
* - when the name starts with slash (`/`) it is treated as an absolute path to a folder
* - when the name contains slash, it is considered to be a relative folder to the
* `User.home` directory, like "`~/`" on unix systems.
* - otherwise, the folder will be created in "`~/.local_storage`" parent directory
* (which also will be created if needed).
*
* - For the native platorms it is not yet implemented (but will be soon).
*
* See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like,
* and `FileDataProvider` class on JVM target.
*/
actual fun defaultNamedStorage(name: String): KVStorage {
TODO("Not yet implemented")
}
@@ -0,0 +1,17 @@
package net.sergeych.synctools
import kotlinx.atomicfu.locks.ReentrantLock
/**
* Native implementation uses `ReentrantLock`]
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val access = ReentrantLock()
override fun lock() {
access.lock()
}
override fun unlock() {
access.unlock()
}
}
@@ -0,0 +1,34 @@
package net.sergeych.synctools
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
private val channel = Channel<Unit>()
actual fun await(milliseconds: Long): Boolean {
return runBlocking {
try {
if( milliseconds > 0) {
withTimeout(milliseconds) {
channel.receive()
true
}
}
else {
channel.receive()
true
}
}
catch(_: TimeoutCancellationException) {
false
}
}
}
actual fun wakeUp() {
runBlocking { channel.send(Unit) }
}
}
@@ -0,0 +1,25 @@
package net.sergeych.bintools
/**
* Create per-platform default named storage.
*
* - In the browser, it uses the `Window.localStorage` prefixing items
* by a string containing the [name]
*
* - In the JVM environment it uses folder-based storage on the file system. The name
* is considered to be a folder name (the whole path which will be automatically created)
* using the following rules:
* - when the name starts with slash (`/`) it is treated as an absolute path to a folder
* - when the name contains slash, it is considered to be a relative folder to the
* `User.home` directory, like "`~/`" on unix systems.
* - otherwise, the folder will be created in "`~/.local_storage`" parent directory
* (which also will be created if needed).
*
* - For the native platorms it is not yet implemented (but will be soon).
*
* See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like,
* and `FileDataProvider` class on JVM target.
*/
actual fun defaultNamedStorage(name: String): KVStorage {
TODO("Not yet implemented")
}
@@ -0,0 +1,17 @@
package net.sergeych.synctools
import kotlinx.atomicfu.locks.ReentrantLock
/**
* Native implementation uses `ReentrantLock`]
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val access = ReentrantLock()
override fun lock() {
access.lock()
}
override fun unlock() {
access.unlock()
}
}
@@ -0,0 +1,34 @@
package net.sergeych.synctools
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
private val channel = Channel<Unit>()
actual fun await(milliseconds: Long): Boolean {
return runBlocking {
try {
if( milliseconds > 0) {
withTimeout(milliseconds) {
channel.receive()
true
}
}
else {
channel.receive()
true
}
}
catch(_: TimeoutCancellationException) {
false
}
}
}
actual fun wakeUp() {
runBlocking { channel.send(Unit) }
}
}
@@ -0,0 +1,42 @@
package net.sergeych.bintools
import kotlinx.browser.localStorage
import net.sergeych.mp_tools.decodeBase64Compact
import net.sergeych.mp_tools.encodeToBase64Compact
import org.w3c.dom.Storage
import org.w3c.dom.set
actual fun defaultNamedStorage(name: String): KVStorage = BrowserKVStorage(name, localStorage)
/**
* Default KV storage in browser. Use if with `localStorage` or `sessionStorage`. It uses
* prefix for storage values not to collide with other data, beware of it.
*/
class BrowserKVStorage(keyPrefix: String, private val bst: Storage) : KVStorage {
private val prefix = "$keyPrefix:"
fun k(key: String) = "$prefix$key"
override fun get(key: String): ByteArray? {
return bst.getItem(k(key))?.decodeBase64Compact()
}
override fun set(key: String, value: ByteArray?) {
val corrected = k(key)
if (value == null)
bst.removeItem(corrected)
else
bst.set(corrected, value.encodeToBase64Compact())
}
override val keys: Set<String>
get() {
val kk = mutableListOf<String>()
for (i in 0 until bst.length) {
val k = bst.key(i) ?: break
if( k.startsWith(prefix)) {
kk += k.substring(prefix.length)
}
}
return kk.toSet()
}
}
@@ -0,0 +1,9 @@
package net.sergeych.synctools
/**
* JS is single-threaded, so we don't need any additional protection:
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
override fun lock() {}
override fun unlock() {}
}
@@ -0,0 +1,13 @@
package net.sergeych.synctools
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
actual fun await(milliseconds: Long): Boolean {
// in JS we can't wait: no threads
return true
}
actual fun wakeUp() {
// in JS we can't wait: no threads
}
}
@@ -0,0 +1,59 @@
package net.sergeych.bintools
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Path
import kotlin.io.path.*
class FileDataProvider(val folder: Path) : DataProvider {
class Source(private val input: InputStream) : DataSource {
override fun readByte(): Byte {
val b = input.read()
if (b < 0) throw DataSource.EndOfData()
return b.toByte()
}
override fun readBytes(size: Int): ByteArray {
return input.readNBytes2(size).also {
if (it.size < size) throw DataSource.EndOfData()
}
}
}
class Sink(private val out: OutputStream) : DataSink {
override fun writeByte(data: Byte) {
out.write(data.toInt())
}
}
override fun <T> read(name: String, f: (DataSource) -> T): T = folder.resolve(name).inputStream().buffered().use {
f(Source(it))
}
override fun write(name: String, f: (DataSink) -> Unit) {
folder.resolve(name).outputStream().use { f(Sink(it)) }
}
override fun delete(name: String) {
println("file: $folder -- $name")
folder.resolve(name).deleteExisting()
}
override fun list(): List<String> = folder
.listDirectoryEntries()
.filter { it.isRegularFile() && it.isReadable() }
.map { it.name }
}
/**
* Compatibility with Java8 and android. Read up to N bytes
*/
fun InputStream.readNBytes2(size: Int): ByteArray {
val result = ByteArray(size)
val len = read(result)
return if (len < size)
result.sliceArray(0..<len)
else
result
}
@@ -0,0 +1,21 @@
package net.sergeych.bintools
import java.nio.file.Paths
import kotlin.io.path.createDirectories
actual fun defaultNamedStorage(name: String): KVStorage {
val rootFolder = Paths.get(when {
// absolute path
name.startsWith("/") -> name
// path - assume the caller knows what to do
name.contains("/") -> name
// simple name - we will create it in the user home:
else -> {
val home = System.getProperty("user.home")
"$home/.local_storage/$name"
}
})
rootFolder.createDirectories()
val provider = FileDataProvider(rootFolder)
return DataKVStorage(provider)
}
@@ -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>()
@@ -0,0 +1,19 @@
package net.sergeych.synctools
import java.util.concurrent.locks.ReentrantLock
/**
* Get the platform-depended implementation of a mutex-protected operation.
* JVM version uses a concealed object synchronization pattern (per-object monitor lock)
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val access = ReentrantLock()
override fun lock() {
access.lock()
}
override fun unlock() {
access.unlock()
}
}
@@ -0,0 +1,22 @@
package net.sergeych.synctools
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
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() }
}
}
@@ -0,0 +1,39 @@
package net.sergeych.bintools
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class KVStorage_jvmKtTest {
@Test
fun testFileStorage() {
val s1 = defaultNamedStorage("test_mp_bintools")
for( n in s1.keys.toList()) s1.delete(n)
assertTrue(s1.keys.isEmpty())
var foo by s1("unknown")
assertEquals(foo, "unknown")
foo = "bar"
assertEquals(foo, "bar")
var answer by s1.optStored<Int>()
assertNull(answer)
answer = 42
assertEquals(answer, 42)
answer = 43
println("----------------------------------------------------------------")
val s2 = defaultNamedStorage("test_mp_bintools")
val foo1 by s2.stored("?", "foo")
val answer1: Int? by s2.optStored("answer")
assertEquals("bar", foo1)
assertEquals(43, answer1)
for( i in 0..< 13 ) {
s2.write("test_$i", "payload_$i")
}
}
}
@@ -0,0 +1,25 @@
package net.sergeych.bintools
/**
* Create per-platform default named storage.
*
* - In the browser, it uses the `Window.localStorage` prefixing items
* by a string containing the [name]
*
* - In the JVM environment it uses folder-based storage on the file system. The name
* is considered to be a folder name (the whole path which will be automatically created)
* using the following rules:
* - when the name starts with slash (`/`) it is treated as an absolute path to a folder
* - when the name contains slash, it is considered to be a relative folder to the
* `User.home` directory, like "`~/`" on unix systems.
* - otherwise, the folder will be created in "`~/.local_storage`" parent directory
* (which also will be created if needed).
*
* - For the native platorms it is not yet implemented (but will be soon).
*
* See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like,
* and `FileDataProvider` class on JVM target.
*/
actual fun defaultNamedStorage(name: String): KVStorage {
TODO("Not yet implemented")
}
@@ -0,0 +1,17 @@
package net.sergeych.synctools
import kotlinx.atomicfu.locks.ReentrantLock
/**
* Native implementation uses `ReentrantLock`]
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val access = ReentrantLock()
override fun lock() {
access.lock()
}
override fun unlock() {
access.unlock()
}
}
@@ -0,0 +1,34 @@
package net.sergeych.synctools
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
private val channel = Channel<Unit>()
actual fun await(milliseconds: Long): Boolean {
return runBlocking {
try {
if( milliseconds > 0) {
withTimeout(milliseconds) {
channel.receive()
true
}
}
else {
channel.receive()
true
}
}
catch(_: TimeoutCancellationException) {
false
}
}
}
actual fun wakeUp() {
runBlocking { channel.send(Unit) }
}
}
@@ -0,0 +1,25 @@
package net.sergeych.bintools
/**
* Create per-platform default named storage.
*
* - In the browser, it uses the `Window.localStorage` prefixing items
* by a string containing the [name]
*
* - In the JVM environment it uses folder-based storage on the file system. The name
* is considered to be a folder name (the whole path which will be automatically created)
* using the following rules:
* - when the name starts with slash (`/`) it is treated as an absolute path to a folder
* - when the name contains slash, it is considered to be a relative folder to the
* `User.home` directory, like "`~/`" on unix systems.
* - otherwise, the folder will be created in "`~/.local_storage`" parent directory
* (which also will be created if needed).
*
* - For the native platorms it is not yet implemented (but will be soon).
*
* See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like,
* and `FileDataProvider` class on JVM target.
*/
actual fun defaultNamedStorage(name: String): KVStorage {
TODO("Not yet implemented")
}
@@ -0,0 +1,17 @@
package net.sergeych.synctools
import kotlinx.atomicfu.locks.ReentrantLock
/**
* Native implementation uses `ReentrantLock`]
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val access = ReentrantLock()
override fun lock() {
access.lock()
}
override fun unlock() {
access.unlock()
}
}
@@ -0,0 +1,34 @@
package net.sergeych.synctools
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
private val channel = Channel<Unit>()
actual fun await(milliseconds: Long): Boolean {
return runBlocking {
try {
if( milliseconds > 0) {
withTimeout(milliseconds) {
channel.receive()
true
}
}
else {
channel.receive()
true
}
}
catch(_: TimeoutCancellationException) {
false
}
}
}
actual fun wakeUp() {
runBlocking { channel.send(Unit) }
}
}
@@ -0,0 +1,25 @@
package net.sergeych.bintools
/**
* Create per-platform default named storage.
*
* - In the browser, it uses the `Window.localStorage` prefixing items
* by a string containing the [name]
*
* - In the JVM environment it uses folder-based storage on the file system. The name
* is considered to be a folder name (the whole path which will be automatically created)
* using the following rules:
* - when the name starts with slash (`/`) it is treated as an absolute path to a folder
* - when the name contains slash, it is considered to be a relative folder to the
* `User.home` directory, like "`~/`" on unix systems.
* - otherwise, the folder will be created in "`~/.local_storage`" parent directory
* (which also will be created if needed).
*
* - For the native platorms it is not yet implemented (but will be soon).
*
* See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like,
* and `FileDataProvider` class on JVM target.
*/
actual fun defaultNamedStorage(name: String): KVStorage {
TODO("Not yet implemented")
}
@@ -0,0 +1,17 @@
package net.sergeych.synctools
import kotlinx.atomicfu.locks.ReentrantLock
/**
* Native implementation uses `ReentrantLock`]
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val access = ReentrantLock()
override fun lock() {
access.lock()
}
override fun unlock() {
access.unlock()
}
}
@@ -0,0 +1,34 @@
package net.sergeych.synctools
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
private val channel = Channel<Unit>()
actual fun await(milliseconds: Long): Boolean {
return runBlocking {
try {
if( milliseconds > 0) {
withTimeout(milliseconds) {
channel.receive()
true
}
}
else {
channel.receive()
true
}
}
catch(_: TimeoutCancellationException) {
false
}
}
}
actual fun wakeUp() {
runBlocking { channel.send(Unit) }
}
}
@@ -0,0 +1,5 @@
package net.sergeych.bintools
actual fun defaultNamedStorage(name: String): KVStorage {
TODO("Not yet implemented")
}
@@ -0,0 +1,17 @@
package net.sergeych.synctools
import kotlinx.atomicfu.locks.ReentrantLock
/**
* Native implementation uses `ReentrantLock`]
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
private val access = ReentrantLock()
override fun lock() {
access.lock()
}
override fun unlock() {
access.unlock()
}
}
@@ -0,0 +1,34 @@
package net.sergeych.synctools
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
private val channel = Channel<Unit>()
actual fun await(milliseconds: Long): Boolean {
return runBlocking {
try {
if( milliseconds > 0) {
withTimeout(milliseconds) {
channel.receive()
true
}
}
else {
channel.receive()
true
}
}
catch(_: TimeoutCancellationException) {
false
}
}
}
actual fun wakeUp() {
runBlocking { channel.send(Unit) }
}
}
@@ -0,0 +1,63 @@
package net.sergeych.bintools
import kotlinx.browser.localStorage
import net.sergeych.mp_tools.decodeBase64Compact
import net.sergeych.mp_tools.encodeToBase64Compact
import org.w3c.dom.Storage
import org.w3c.dom.set
/**
* Create per-platform default named storage.
*
* - In the browser, it uses the `Window.localStorage` prefixing items
* by a string containing the [name]
*
* - In the JVM environment it uses folder-based storage on the file system. The name
* is considered to be a folder name (the whole path which will be automatically created)
* using the following rules:
* - when the name starts with slash (`/`) it is treated as an absolute path to a folder
* - when the name contains slash, it is considered to be a relative folder to the
* `User.home` directory, like "`~/`" on unix systems.
* - otherwise, the folder will be created in "`~/.local_storage`" parent directory
* (which also will be created if needed).
*
* - For the native platorms it is not yet implemented (but will be soon).
*
* See [DataKVStorage] and [DataProvider] to implement a KVStorage on filesystems and like,
* and `FileDataProvider` class on JVM target.
*/
actual fun defaultNamedStorage(name: String): KVStorage = BrowserKVStorage(name, localStorage)
/**
* Default KV storage in browser. Use if with `localStorage` or `sessionStorage`. It uses
* prefix for storage values not to collide with other data, beware of it.
*/
class BrowserKVStorage(keyPrefix: String, private val bst: Storage) : KVStorage {
private val prefix = "$keyPrefix:"
fun k(key: String) = "$prefix$key"
override fun get(key: String): ByteArray? {
return bst.getItem(k(key))?.decodeBase64Compact()
}
override fun set(key: String, value: ByteArray?) {
val corrected = k(key)
if (value == null)
bst.removeItem(corrected)
else
bst.set(corrected, value.encodeToBase64Compact())
}
override val keys: Set<String>
get() {
val kk = mutableListOf<String>()
for (i in 0 until bst.length) {
val k = bst.key(i) ?: break
if( k.startsWith(prefix)) {
kk += k.substring(prefix.length)
}
}
return kk.toSet()
}
}
@@ -0,0 +1,27 @@
package net.sergeych.synctools
import kotlinx.atomicfu.locks.ReentrantLock
/**
* Get the platform-depended implementation of a mutex. It does nothing in the
* browser and use appropriate mechanics on JVM and native targets. See
* [ProtectedOpImplementation.invoke], [ProtectedOpImplementation.withLock]
* ```kotlin
* val op = ProtectedOp()
* //...
* op {
* // mutually exclusive execution
* println("sequential execution here")
* }
* ~~~
*/
actual fun ProtectedOp(): ProtectedOpImplementation = object: ProtectedOpImplementation {
val rlock = ReentrantLock()
override fun lock() {
rlock.lock()
}
override fun unlock() {
rlock.unlock()
}
}
@@ -0,0 +1,19 @@
package net.sergeych.synctools
/**
* Platform-independent interface to thread wait/notify. Does nothing in JS/browser,
* and uses appropriate mechanics on other platforms.
*
* Wasm is effictively songle threaded at the moment
*/
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class WaitHandle {
actual fun await(milliseconds: Long): Boolean {
// in JS we can't wait: no threads
return true
}
actual fun wakeUp() {
// in JS we can't wait: no threads
}
}
+37
View File
@@ -0,0 +1,37 @@
import net.sergeych.bintools.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class StorageTest {
@Test
fun storageTest() {
val s1 = defaultNamedStorage("test_mp_bintools")
for (n in s1.keys.toList()) s1.delete(n)
assertTrue(s1.keys.isEmpty())
var foo by s1("unknown")
assertEquals(foo, "unknown")
foo = "bar"
assertEquals(foo, "bar")
var answer by s1.optStored<Int>()
assertNull(answer)
answer = 42
assertEquals(answer, 42)
answer = 43
val s2 = defaultNamedStorage("test_mp_bintools")
val foo1 by s2.stored("?", "foo")
val answer1: Int? by s2.optStored("answer")
assertEquals("bar", foo1)
assertEquals(43, answer1)
for (i in 0..<13) {
s2.write("test_$i", "payload_$i")
}
}
}