From 60397c9d09c09ea25df27cd6010bbba9e47c8752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 14 Dec 2025 23:28:49 +0100 Subject: [PATCH 1/3] docs(bson-multiplatform): Document the implementation strategy --- bson-multiplatform/src/commonMain/kotlin/Bson.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bson-multiplatform/src/commonMain/kotlin/Bson.kt b/bson-multiplatform/src/commonMain/kotlin/Bson.kt index 8f712455..5fd1f835 100644 --- a/bson-multiplatform/src/commonMain/kotlin/Bson.kt +++ b/bson-multiplatform/src/commonMain/kotlin/Bson.kt @@ -24,6 +24,14 @@ import opensavvy.ktmongo.bson.multiplatform.impl.read.MultiplatformArrayReader import opensavvy.ktmongo.bson.multiplatform.impl.read.MultiplatformDocumentReader import opensavvy.ktmongo.dsl.LowLevelApi +/** + * Pure Kotlin BSON document implementation. + * + * ### Implementation + * + * Instead of decoding the BSON document into subdocuments, this class stores the raw bytes in BSON representation. + * Fields are decoded lazily when searched via the [reader]. + */ class Bson internal constructor( private val factory: BsonFactory, private val data: Bytes, @@ -41,6 +49,14 @@ class Bson internal constructor( reader().toString() } +/** + * Pure Kotlin BSON array implementation. + * + * ### Implementation + * + * Instead of decoding the BSON array into subdocuments, this class stores the raw bytes in BSON representation. + * Fields are decoded lazily when searched via the [reader]. + */ class BsonArray internal constructor( private val factory: BsonFactory, private val data: Bytes, -- 2.51.2 From a1dc386818ac8431fd3fdb0b6e7b37493352fad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 14 Dec 2025 23:39:53 +0100 Subject: [PATCH 2/3] perf(bson-multiplatform): Reuse the reader instances for Bson and BsonArray --- bson-multiplatform/src/commonMain/kotlin/Bson.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bson-multiplatform/src/commonMain/kotlin/Bson.kt b/bson-multiplatform/src/commonMain/kotlin/Bson.kt index 5fd1f835..652df312 100644 --- a/bson-multiplatform/src/commonMain/kotlin/Bson.kt +++ b/bson-multiplatform/src/commonMain/kotlin/Bson.kt @@ -41,8 +41,12 @@ class Bson internal constructor( override fun toByteArray(): ByteArray = data.toByteArray() @LowLevelApi - override fun reader(): BsonDocumentReader = + private val reader by lazy(LazyThreadSafetyMode.PUBLICATION) { MultiplatformDocumentReader(factory, data) + } + + @LowLevelApi + override fun reader(): BsonDocumentReader = reader @OptIn(LowLevelApi::class) override fun toString(): String = @@ -66,8 +70,12 @@ class BsonArray internal constructor( override fun toByteArray(): ByteArray = data.toByteArray() @LowLevelApi - override fun reader(): BsonArrayReader = + private val reader by lazy(LazyThreadSafetyMode.PUBLICATION) { MultiplatformArrayReader(factory, data) + } + + @LowLevelApi + override fun reader(): BsonArrayReader = reader @OptIn(LowLevelApi::class) override fun toString(): String = -- 2.51.2 From fa9e05544ccbaffd1536a6c29959633721ba2e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 14 Dec 2025 23:48:20 +0100 Subject: [PATCH 3/3] feat(bson-multiplatform): Add the 'Bson.eager()' method to eagerly initialize a BSON hierarchy --- .../src/commonMain/kotlin/Bson.kt | 36 +++++++++++++++++++ .../impl/read/MultiplatformArrayReader.kt | 8 +++++ .../impl/read/MultiplatformBsonValueReader.kt | 16 +++++++-- .../impl/read/MultiplatformDocumentReader.kt | 8 +++++ 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/bson-multiplatform/src/commonMain/kotlin/Bson.kt b/bson-multiplatform/src/commonMain/kotlin/Bson.kt index 652df312..ad1950b2 100644 --- a/bson-multiplatform/src/commonMain/kotlin/Bson.kt +++ b/bson-multiplatform/src/commonMain/kotlin/Bson.kt @@ -31,6 +31,9 @@ import opensavvy.ktmongo.dsl.LowLevelApi * * Instead of decoding the BSON document into subdocuments, this class stores the raw bytes in BSON representation. * Fields are decoded lazily when searched via the [reader]. + * + * Because the reader is lazy, it is not thread-safe. + * If you want to use this instance without external synchronization, call [eager]. */ class Bson internal constructor( private val factory: BsonFactory, @@ -48,6 +51,21 @@ class Bson internal constructor( @LowLevelApi override fun reader(): BsonDocumentReader = reader + /** + * Scans this entire document recursively to find all the fields. + * + * By default, [Bson] lazily scans for fields. + * This is particularly beneficial if there is more data than you are interested in. + * However, this means the [reader] may discover fields as it is being used, which is not thread-safe. + * + * Instead, you can call this function to force a scan of the entire hierarchy. + * After this function returns, [reader] and all the values returned by it are thread-safe and immutable. + */ + @OptIn(LowLevelApi::class) + fun eager() { + reader.eager() + } + @OptIn(LowLevelApi::class) override fun toString(): String = reader().toString() @@ -60,6 +78,9 @@ class Bson internal constructor( * * Instead of decoding the BSON array into subdocuments, this class stores the raw bytes in BSON representation. * Fields are decoded lazily when searched via the [reader]. + * + * Because the reader is lazy, it is not thread-safe. + * If you want to use this instance without external synchronization, call [eager]. */ class BsonArray internal constructor( private val factory: BsonFactory, @@ -77,6 +98,21 @@ class BsonArray internal constructor( @LowLevelApi override fun reader(): BsonArrayReader = reader + /** + * Scans this entire array recursively to find all the fields. + * + * By default, [BsonArray] lazily scans for items. + * This is particularly beneficial if there is more data than you are interested in. + * However, this means the [reader] may discover items as it is being used, which is not thread-safe. + * + * Instead, you can call this function to force a scan of the entire hierarchy. + * After this function returns, [reader] and all the values returned by it are thread-safe and immutable. + */ + @OptIn(LowLevelApi::class) + fun eager() { + reader.eager() + } + @OptIn(LowLevelApi::class) override fun toString(): String = reader().toString() diff --git a/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformArrayReader.kt b/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformArrayReader.kt index d9524c8f..5ddcf92d 100644 --- a/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformArrayReader.kt +++ b/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformArrayReader.kt @@ -79,6 +79,14 @@ internal class MultiplatformArrayReader( override fun asValue(): BsonValueReader = MultiplatformBsonValueReader(factory, BsonType.Array, bytesWithHeader) + internal fun eager() { + scanUntil(null) + + for (field in fields) { + field.eager() + } + } + @OptIn(ExperimentalSerializationApi::class) override fun read(type: KType, klass: KClass): T? { val decoder = BsonDecoderTopLevel(EmptySerializersModule(), factory, bytesWithHeader) diff --git a/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformBsonValueReader.kt b/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformBsonValueReader.kt index 74b5d256..45feb775 100644 --- a/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformBsonValueReader.kt +++ b/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformBsonValueReader.kt @@ -19,7 +19,9 @@ package opensavvy.ktmongo.bson.multiplatform.impl.read import kotlinx.serialization.KSerializer import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer -import opensavvy.ktmongo.bson.* +import opensavvy.ktmongo.bson.BsonReaderException +import opensavvy.ktmongo.bson.BsonType +import opensavvy.ktmongo.bson.BsonValueReader import opensavvy.ktmongo.bson.multiplatform.BsonFactory import opensavvy.ktmongo.bson.multiplatform.Bytes import opensavvy.ktmongo.bson.multiplatform.RawBsonWriter @@ -205,13 +207,13 @@ internal class MultiplatformBsonValueReader( } @LowLevelApi - override fun readDocument(): BsonDocumentReader { + override fun readDocument(): MultiplatformDocumentReader { checkType(BsonType.Document) return MultiplatformDocumentReader(factory, bytes) } @LowLevelApi - override fun readArray(): BsonArrayReader { + override fun readArray(): MultiplatformArrayReader { checkType(BsonType.Array) return MultiplatformArrayReader(factory, bytes) } @@ -221,6 +223,14 @@ internal class MultiplatformBsonValueReader( writer.writeArbitrary(bytes) } + internal fun eager() { + when (type) { + BsonType.Document -> readDocument().eager() + BsonType.Array -> readArray().eager() + else -> {} + } + } + @OptIn(ExperimentalTime::class) @Suppress("DEPRECATION") override fun toString(): String = when (type) { diff --git a/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformDocumentReader.kt b/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformDocumentReader.kt index 6b3900ba..26629104 100644 --- a/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformDocumentReader.kt +++ b/bson-multiplatform/src/commonMain/kotlin/impl/read/MultiplatformDocumentReader.kt @@ -149,6 +149,14 @@ internal class MultiplatformDocumentReader( override fun asValue(): BsonValueReader = MultiplatformBsonValueReader(factory, BsonType.Document, bytesWithHeader) + internal fun eager() { + scanUntil(null) + + for (field in fields.values) { + field.eager() + } + } + @Suppress("UNCHECKED_CAST") override fun read(type: KType, klass: KClass): T? { val decoder = BsonDecoder(EmptySerializersModule(), this.asValue()) -- 2.51.2