Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ insert_final_newline = true
indent_size = 4

[*.{kt,kts}]
ij_kotlin_imports_layout = *
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_any_case_statement_on_separate_line = false
# Defines the layout; omitting '*' usually forces individual imports
ij_kotlin_imports_layout =
# Sets the threshold to a very high number so wildcards are never triggered
ij_kotlin_name_count_to_use_star_import = 999
ij_kotlin_name_count_to_use_star_import_for_members = 999

[{Makefile,*.go}]
indent_style = tab
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ jobs:
- name: Generate Test Coverage Xml Report
run: ./gradlew koverXmlReport

- name: Run codacy-coverage-reporter
uses: codacy/codacy-coverage-reporter-action@v1
with:
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
coverage-reports: ./build/reports/jacoco/test/jacocoTestReport.xml
# - name: Run codacy-coverage-reporter
# uses: codacy/codacy-coverage-reporter-action@v1
# with:
# project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
# coverage-reports: ./build/reports/jacoco/test/jacocoTestReport.xml

- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ atlassian-ide-plugin.xml
*.zip
*.tar.gz
*.rar

local.properties
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# Gradle files
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<a href="https://hitsofcode.com/github/ashtanko/kotlab/view?branch=main"><img alt="Hits-of-Code" src="https://hitsofcode.com/github/ashtanko/kotlab?branch=main"/></a>
<a href="https://app.fossa.com/projects/git%2Bgithub.qkg1.top%2Fashtanko%2Falgorithms-in-depth?ref=badge_shield&issueType=license"><img alt="FOSSA Status" src="https://app.fossa.com/api/projects/git%2Bgithub.qkg1.top%2Fashtanko%2Falgorithms-in-depth.svg?type=shield&issueType=license"/></a>
<a href="https://ktlint.github.io/"><img alt="CodeStyle" src="https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg"/></a>
<a href="https://kotlinlang.org/"><img alt="Kotlin Version" src="https://img.shields.io/badge/kotlin-2.2.20-blue.svg"/></a>
<a href="https://kotlinlang.org/"><img alt="Kotlin Version" src="https://img.shields.io/badge/kotlin-2.2.21-blue.svg"/></a>
<a href="https://sonarcloud.io/summary/new_code?id=ashtanko_kotlab"><img alt="Quality Gate Status" src="https://sonarcloud.io/api/project_badges/measure?project=ashtanko_kotlab&metric=alert_status"/></a>
<a href="https://sonarcloud.io/summary/new_code?id=ashtanko_kotlab"><img alt="Bugs" src="https://sonarcloud.io/api/project_badges/measure?project=ashtanko_kotlab&metric=bugs"/></a>
<a href="https://sonarcloud.io/summary/new_code?id=ashtanko_kotlab"><img alt="Code Smells" src="https://sonarcloud.io/api/project_badges/measure?project=ashtanko_kotlab&metric=code_smells"/></a>
Expand All @@ -20,22 +20,22 @@

### Metrics
```text
15345 number of properties
10588 number of functions
8955 number of classes
240 number of packages
3545 number of kt files
15358 number of properties
10598 number of functions
8962 number of classes
241 number of packages
3550 number of kt files
```


### Complexity Report
```text
267779 lines of code (loc)
166654 source lines of code (sloc)
121749 logical lines of code (lloc)
72563 comment lines of code (cloc)
25144 cyclomatic complexity (mcc)
20480 cognitive complexity
267971 lines of code (loc)
166790 source lines of code (sloc)
121831 logical lines of code (lloc)
72582 comment lines of code (cloc)
25155 cyclomatic complexity (mcc)
20482 cognitive complexity
0 number of total code smells
43 comment source ratio
206 mcc per 1,000 lloc
Expand Down
29 changes: 29 additions & 0 deletions api/Kotlin-Lab.api
Original file line number Diff line number Diff line change
Expand Up @@ -19788,6 +19788,35 @@ public final class dev/shtanko/concurrency/coroutines/flow/StateFlowExample {
public final fun updateState (Ljava/lang/String;)V
}

public final class dev/shtanko/concurrency/coroutines/flow/cold/FakeSearchApi : dev/shtanko/concurrency/coroutines/flow/cold/SearchApi {
public fun <init> ()V
public fun search (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract interface class dev/shtanko/concurrency/coroutines/flow/cold/SearchApi {
public abstract fun search (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class dev/shtanko/concurrency/coroutines/flow/cold/SearchManager {
public fun <init> (Ldev/shtanko/concurrency/coroutines/flow/cold/SearchApi;)V
public final fun getSuggestions (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow;
public static synthetic fun getSuggestions$default (Ldev/shtanko/concurrency/coroutines/flow/cold/SearchManager;Lkotlinx/coroutines/flow/Flow;JILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
}

public abstract interface class dev/shtanko/concurrency/coroutines/flow/cold/StockApi {
public abstract fun fetchPrice (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class dev/shtanko/concurrency/coroutines/flow/cold/StockApiKt {
public static final fun tickerFlow (J)Lkotlinx/coroutines/flow/Flow;
}

public final class dev/shtanko/concurrency/coroutines/flow/cold/StockRepository {
public fun <init> (Ldev/shtanko/concurrency/coroutines/flow/cold/StockApi;J)V
public synthetic fun <init> (Ldev/shtanko/concurrency/coroutines/flow/cold/StockApi;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getStockPrice (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow;
}

public final class dev/shtanko/concurrency/coroutines/flow/operators/FlowCombiningOperators {
public static final field INSTANCE Ldev/shtanko/concurrency/coroutines/flow/operators/FlowCombiningOperators;
}
Expand Down
2 changes: 1 addition & 1 deletion config/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<a href="https://hitsofcode.com/github/ashtanko/kotlab/view?branch=main"><img alt="Hits-of-Code" src="https://hitsofcode.com/github/ashtanko/kotlab?branch=main"/></a>
<a href="https://app.fossa.com/projects/git%2Bgithub.qkg1.top%2Fashtanko%2Falgorithms-in-depth?ref=badge_shield&issueType=license"><img alt="FOSSA Status" src="https://app.fossa.com/api/projects/git%2Bgithub.qkg1.top%2Fashtanko%2Falgorithms-in-depth.svg?type=shield&issueType=license"/></a>
<a href="https://ktlint.github.io/"><img alt="CodeStyle" src="https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg"/></a>
<a href="https://kotlinlang.org/"><img alt="Kotlin Version" src="https://img.shields.io/badge/kotlin-2.2.20-blue.svg"/></a>
<a href="https://kotlinlang.org/"><img alt="Kotlin Version" src="https://img.shields.io/badge/kotlin-2.2.21-blue.svg"/></a>
<a href="https://sonarcloud.io/summary/new_code?id=ashtanko_kotlab"><img alt="Quality Gate Status" src="https://sonarcloud.io/api/project_badges/measure?project=ashtanko_kotlab&metric=alert_status"/></a>
<a href="https://sonarcloud.io/summary/new_code?id=ashtanko_kotlab"><img alt="Bugs" src="https://sonarcloud.io/api/project_badges/measure?project=ashtanko_kotlab&metric=bugs"/></a>
<a href="https://sonarcloud.io/summary/new_code?id=ashtanko_kotlab"><img alt="Code Smells" src="https://sonarcloud.io/api/project_badges/measure?project=ashtanko_kotlab&metric=code_smells"/></a>
Expand Down
8 changes: 0 additions & 8 deletions local.properties

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.shtanko.concurrency.coroutines.flow.cold

import kotlinx.coroutines.delay

interface SearchApi {
suspend fun search(query: String): List<String>
}

// Simple implementation for testing/demo
@Suppress("MagicNumber")
class FakeSearchApi : SearchApi {
override suspend fun search(query: String): List<String> {
delay(500) // Simulate network delay
val database = listOf("Kotlin", "Coroutines", "Flow", "Ktor", "KMP", "Android")
return database.filter { it.contains(query, ignoreCase = true) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.shtanko.concurrency.coroutines.flow.cold

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow

class SearchManager(private val api: SearchApi) {
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
fun getSuggestions(
queryFlow: Flow<String>,
debounceMillis: Long = 300L, // Default for production
): Flow<List<String>> = queryFlow
.debounce(debounceMillis)
.filter { it.isNotBlank() }
.distinctUntilChanged()
.flatMapLatest { query ->
flow { emit(api.search(query)) }
}
.catch { emit(emptyList()) }
Comment thread
ashtanko marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dev.shtanko.concurrency.coroutines.flow.cold

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map

interface StockApi {
suspend fun fetchPrice(symbol: String): Double
}

fun tickerFlow(interval: Long) = flow {
while (true) {
emit(Unit)
delay(interval)
}
}

class StockRepository(
private val api: StockApi,
private val refreshInterval: Long = 5000L,
) {
fun getStockPrice(symbol: String): Flow<Double> =
tickerFlow(refreshInterval).map { api.fetchPrice(symbol) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package dev.shtanko.concurrency.coroutines.flow.cold

import app.cash.turbine.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

@OptIn(ExperimentalCoroutinesApi::class)
class SearchManagerTest {

private val fakeApi = FakeSearchApi()
private val manager = SearchManager(fakeApi)

@Test
fun `search flow should debounce and return results`() = runTest {
// Use a buffer of 1 so emissions aren't lost during setup
val queryInput = MutableSharedFlow<String>(replay = 1)
val testDebounce = 100L

manager.getSuggestions(queryInput, debounceMillis = testDebounce).test {
// Emit "K"
queryInput.emit("K")
advanceTimeBy(50) // Less than debounce

// Emit "Ko" - this should reset the timer for "K"
queryInput.emit("Ko")

// Advance exactly enough to trigger to debounce
advanceTimeBy(testDebounce + 1)

val result = awaitItem()
assertEquals(listOf("Kotlin"), result)

cancelAndIgnoreRemainingEvents()
}
}

@Test
fun `search flow should ignore duplicate consecutive queries`() = runTest {
val queryInput = MutableSharedFlow<String>(replay = 1)
val testDebounce = 100L

manager.getSuggestions(queryInput, debounceMillis = testDebounce).test {
// First search
queryInput.emit("Kotlin")
advanceTimeBy(testDebounce + 1)
awaitItem()

// Immediate duplicate search
queryInput.emit("Kotlin")
advanceTimeBy(testDebounce + 1)

// Assert that no new list was emitted
expectNoEvents()
cancelAndIgnoreRemainingEvents()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package dev.shtanko.concurrency.coroutines.flow.cold

import app.cash.turbine.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
class StockRepositoryTest {

private val mockApi = mock<StockApi>()

@Test
fun `test will not hang because turbine cancels the scope`() = runTest {
// We set a small interval
val repository = StockRepository(mockApi, refreshInterval = 100L)
val symbol = "AAPL"

whenever(mockApi.fetchPrice(symbol)).thenReturn(150.0, 151.0)

// The 'test' extension starts the 'while(true)' loop
repository.getStockPrice(symbol).test {

assertEquals(150.0, awaitItem())

advanceTimeBy(100L)

assertEquals(151.0, awaitItem())

// This is the "Kill Switch"
// It throws a CancellationException inside the Repository's loop
cancelAndIgnoreRemainingEvents()
}

// If we reach here, the loop is confirmed DEAD.
}

@Test
fun `alternative using take to force a finite flow`() = runTest {
val repository = StockRepository(mockApi, refreshInterval = 0L)
whenever(mockApi.fetchPrice("AAPL")).thenReturn(100.0, 101.0, 102.0)

// 'take(3)' turns the endless flow into a finite one that
// automatically closes after 3 emissions.
val results = repository.getStockPrice("AAPL")
.take(3)
.toList() // Collects everything into a list

assertEquals(3, results.size)
assertEquals(102.0, results.last())
}
}
Loading