Skip to content

Commit 3b383d6

Browse files
committed
feat(model-client): history interval query for given list of split points
1 parent e87a86f commit 3b383d6

File tree

16 files changed

+283
-181
lines changed

16 files changed

+283
-181
lines changed

model-client/src/commonMain/kotlin/org/modelix/model/client2/IModelClientV2.kt

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package org.modelix.model.client2
22

33
import io.ktor.http.Url
4-
import kotlinx.datetime.Instant
4+
import org.modelix.datastructures.history.IHistoryQueries
55
import org.modelix.datastructures.objects.ObjectHash
66
import org.modelix.kotlin.utils.DeprecationInfo
77
import org.modelix.model.IVersion
@@ -13,8 +13,6 @@ import org.modelix.model.lazy.BranchReference
1313
import org.modelix.model.lazy.RepositoryId
1414
import org.modelix.model.server.api.RepositoryConfig
1515
import org.modelix.modelql.core.IMonoStep
16-
import kotlin.time.Duration
17-
import kotlin.time.Duration.Companion.minutes
1816

1917
/**
2018
* This interface is meant exclusively for model client usage.
@@ -110,58 +108,5 @@ interface IModelClientV2 {
110108

111109
fun getFrontendUrl(branch: BranchReference): Url
112110

113-
/**
114-
* Splits the history between versions where the time difference is greater or equal to [delay].
115-
*
116-
* @param headVersion starting point for history computations.
117-
* @param timeRange return versions in this time range only
118-
* @param delay time between two changes after which it is considered to be a new session
119-
*/
120-
suspend fun getHistorySessions(
121-
repositoryId: RepositoryId,
122-
headVersion: ObjectHash,
123-
timeRange: ClosedRange<Instant>? = null,
124-
delay: Duration = 5.minutes,
125-
): List<HistoryInterval>
126-
127-
/**
128-
*
129-
* @param headVersion starting point for history computations.
130-
* @param timeRange return versions in this time range only
131-
* @param interval splits the timeline into equally sized intervals and returns a summary of the contained versions
132-
*/
133-
suspend fun getHistoryIntervals(
134-
repositoryId: RepositoryId,
135-
headVersion: ObjectHash,
136-
timeRange: ClosedRange<Instant>?,
137-
interval: Duration,
138-
): List<HistoryInterval>
139-
140-
/**
141-
* A paginated view on the list of all versions in the history sorted by their timestamp. Latest version first.
142-
* @param headVersion starting point for history computations. For a paginated view this value should be the same
143-
* and the value for [skip] should be incremented instead. Only then it's guaranteed that the returned list
144-
* is complete.
145-
*/
146-
suspend fun getHistoryRange(
147-
repositoryId: RepositoryId,
148-
headVersion: ObjectHash,
149-
skip: Long = 0L,
150-
limit: Long = 1000L,
151-
): List<IVersion>
111+
fun queryHistory(repositoryId: RepositoryId, headVersion: ObjectHash): IHistoryQueries
152112
}
153-
154-
/**
155-
* A summary of a range of versions.
156-
*/
157-
data class HistoryInterval(
158-
val firstVersionHash: ObjectHash,
159-
val lastVersionHash: ObjectHash,
160-
/**
161-
* Number of versions contained in this interval.
162-
*/
163-
val size: Long,
164-
val minTime: Instant,
165-
val maxTime: Instant,
166-
val authors: Set<String>,
167-
)

model-client/src/commonMain/kotlin/org/modelix/model/client2/ModelClientV2.kt

Lines changed: 6 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ import kotlinx.coroutines.flow.emptyFlow
3636
import kotlinx.coroutines.flow.flow
3737
import kotlinx.coroutines.flow.map
3838
import kotlinx.coroutines.launch
39-
import kotlinx.datetime.Instant
4039
import mu.KotlinLogging
41-
import org.modelix.datastructures.model.HistoryIndexNode
40+
import org.modelix.datastructures.history.HistoryIndexNode
41+
import org.modelix.datastructures.history.HistoryQueries
42+
import org.modelix.datastructures.history.IHistoryQueries
4243
import org.modelix.datastructures.objects.IObjectGraph
4344
import org.modelix.datastructures.objects.Object
4445
import org.modelix.datastructures.objects.ObjectHash
@@ -92,8 +93,6 @@ import org.modelix.model.server.api.v2.toMap
9293
import org.modelix.modelql.client.ModelQLClient
9394
import org.modelix.modelql.core.IMonoStep
9495
import org.modelix.streams.IExecutableStream
95-
import org.modelix.streams.getSuspending
96-
import org.modelix.streams.iterateSuspending
9796
import kotlin.time.Duration
9897
import kotlin.time.Duration.Companion.seconds
9998

@@ -333,102 +332,11 @@ class ModelClientV2(
333332
}
334333
}
335334

336-
/**
337-
* Splits the history into intervals where the time difference between two versions is less or equal to [delay].
338-
*/
339-
override suspend fun getHistorySessions(
340-
repositoryId: RepositoryId,
341-
headVersion: ObjectHash,
342-
timeRange: ClosedRange<Instant>?,
343-
delay: Duration,
344-
): List<HistoryInterval> {
345-
val index: Object<HistoryIndexNode> = getHistoryIndex(repositoryId, headVersion)
346-
val sessions = ArrayList<HistoryInterval>()
347-
var previousMinTime = Instant.fromEpochSeconds(Long.MAX_VALUE)
348-
349-
// In the worst case two adjacent intervals contain a single entry directly at the border.
350-
// The maximum difference between these two entries is less than two times the interval.
351-
val interval = delay / 2
352-
353-
index.data.splitAtInterval(interval, timeRange).iterateSuspending(index.graph) {
354-
if (previousMinTime - it.maxTime >= delay) {
355-
sessions += HistoryInterval(
356-
firstVersionHash = it.firstVersion.getHash(),
357-
lastVersionHash = it.lastVersion.getHash(),
358-
size = it.size,
359-
minTime = it.minTime,
360-
maxTime = it.maxTime,
361-
authors = it.authors,
362-
)
363-
} else {
364-
val entry = sessions[sessions.lastIndex]
365-
sessions[sessions.lastIndex] = HistoryInterval(
366-
firstVersionHash = it.firstVersion.getHash(),
367-
lastVersionHash = entry.lastVersionHash,
368-
size = entry.size + it.size,
369-
minTime = minOf(entry.minTime, it.minTime),
370-
maxTime = maxOf(entry.maxTime, it.maxTime),
371-
authors = entry.authors + it.authors,
372-
)
373-
}
374-
previousMinTime = it.minTime
375-
}
376-
377-
return sessions
378-
}
379-
380-
override suspend fun getHistoryIntervals(
381-
repositoryId: RepositoryId,
382-
headVersion: ObjectHash,
383-
timeRange: ClosedRange<Instant>?,
384-
interval: Duration,
385-
): List<HistoryInterval> {
386-
val index: Object<HistoryIndexNode> = getHistoryIndex(repositoryId, headVersion)
387-
val mergedEntries = ArrayList<HistoryInterval>()
388-
var previousIntervalId: Long = Long.MAX_VALUE
389-
390-
index.data.splitAtInterval(interval, timeRange).iterateSuspending(index.graph) {
391-
val intervalId = it.maxTime.epochSeconds / interval.inWholeSeconds
392-
check(intervalId <= previousIntervalId)
393-
if (intervalId == previousIntervalId) {
394-
val entry = mergedEntries[mergedEntries.lastIndex]
395-
mergedEntries[mergedEntries.lastIndex] = HistoryInterval(
396-
firstVersionHash = it.firstVersion.getHash(),
397-
lastVersionHash = entry.lastVersionHash,
398-
size = entry.size + it.size,
399-
minTime = minOf(entry.minTime, it.minTime),
400-
maxTime = maxOf(entry.maxTime, it.maxTime),
401-
authors = entry.authors + it.authors,
402-
)
403-
} else {
404-
previousIntervalId = intervalId
405-
mergedEntries += HistoryInterval(
406-
firstVersionHash = it.firstVersion.getHash(),
407-
lastVersionHash = it.lastVersion.getHash(),
408-
size = it.size,
409-
minTime = it.minTime,
410-
maxTime = it.maxTime,
411-
authors = it.authors,
412-
)
413-
}
414-
}
415-
416-
return mergedEntries
417-
}
418-
419-
override suspend fun getHistoryRange(
335+
override fun queryHistory(
420336
repositoryId: RepositoryId,
421337
headVersion: ObjectHash,
422-
skip: Long,
423-
limit: Long,
424-
): List<IVersion> {
425-
val index: Object<HistoryIndexNode> = getHistoryIndex(repositoryId, headVersion)
426-
return index.data.getRange(skip until (limit + skip))
427-
.flatMapOrdered { it.getAllVersionsReversed() }
428-
.flatMapOrdered { it.resolve() }
429-
.map { CLVersion(it) }
430-
.toList()
431-
.getSuspending(index.graph)
338+
): IHistoryQueries {
339+
return HistoryQueries { getHistoryIndex(repositoryId, headVersion) }
432340
}
433341

434342
suspend fun getHistoryIndex(

model-client/src/jsMain/kotlin/org/modelix/model/client2/ClientJS.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,10 @@ internal class ClientJSImpl(private val modelClient: ModelClientV2) : ClientJS {
205205

206206
override fun getHistoryRange(repositoryId: String, headVersion: String, skip: Int, limit: Int) =
207207
GlobalScope.promise {
208-
modelClient.getHistoryRange(
208+
modelClient.queryHistory(
209209
RepositoryId(repositoryId),
210210
ObjectHash(headVersion),
211+
).range(
211212
skip.toLong(),
212213
limit.toLong(),
213214
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.modelix.datastructures.history
2+
3+
import kotlinx.datetime.Instant
4+
import kotlin.time.Duration
5+
6+
class EquidistantIntervalsSpec(val duration: Duration) : IntervalsSpec {
7+
override fun getIntervalIndex(time: Instant): Long {
8+
return time.epochSeconds / duration.inWholeSeconds
9+
}
10+
11+
override fun includes(time: Instant): Boolean = true
12+
13+
override fun intersects(range: ClosedRange<Instant>): Boolean = true
14+
}
Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.modelix.datastructures.model
1+
package org.modelix.datastructures.history
22

33
import kotlinx.datetime.Instant
44
import org.modelix.datastructures.objects.IObjectData
@@ -23,7 +23,6 @@ import org.modelix.streams.plus
2323
import kotlin.math.abs
2424
import kotlin.math.max
2525
import kotlin.math.min
26-
import kotlin.time.Duration
2726

2827
sealed class HistoryIndexNode : IObjectData {
2928
abstract val firstVersion: ObjectReference<CPVersion>
@@ -46,7 +45,7 @@ sealed class HistoryIndexNode : IObjectData {
4645
* For the same interval multiple nodes may be returned.
4746
* Latest entry is returned first.
4847
*/
49-
abstract fun splitAtInterval(interval: Duration, timeRangeFilter: ClosedRange<Instant>?): IStream.Many<HistoryIndexNode>
48+
abstract fun splitAtInterval(intervalSpec: IntervalsSpec): IStream.Many<HistoryIndexNode>
5049

5150
fun concat(
5251
self: Object<HistoryIndexNode>,
@@ -213,8 +212,8 @@ data class HistoryIndexLeafNode(
213212
}
214213
}
215214

216-
override fun splitAtInterval(interval: Duration, timeRangeFilter: ClosedRange<Instant>?): IStream.Many<HistoryIndexNode> {
217-
return if (timeRangeFilter == null || timeRangeFilter.contains(time)) IStream.of(this) else IStream.empty()
215+
override fun splitAtInterval(intervalSpec: IntervalsSpec): IStream.Many<HistoryIndexNode> {
216+
return if (intervalSpec.includes(time)) IStream.of(this) else IStream.empty()
218217
}
219218
}
220219

@@ -335,17 +334,17 @@ data class HistoryIndexRangeNode(
335334
}.flatten()
336335
}
337336

338-
override fun splitAtInterval(interval: Duration, timeRangeFilter: ClosedRange<Instant>?): IStream.Many<HistoryIndexNode> {
339-
if (timeRangeFilter != null && !timeRangeFilter.intersects(timeRange)) return IStream.empty()
337+
override fun splitAtInterval(intervalSpec: IntervalsSpec): IStream.Many<HistoryIndexNode> {
338+
if (!intervalSpec.intersects(timeRange)) return IStream.empty()
340339

341-
val intervalIndex1 = timeRange.start.epochSeconds / interval.inWholeSeconds
342-
val intervalIndex2 = timeRange.endInclusive.epochSeconds / interval.inWholeSeconds
343-
if (intervalIndex1 == intervalIndex2) {
340+
val intervalIndex1 = intervalSpec.getIntervalIndex(timeRange.start)
341+
val intervalIndex2 = intervalSpec.getIntervalIndex(timeRange.endInclusive)
342+
if (intervalIndex1 == intervalIndex2 && intervalIndex1 >= 0) {
344343
return IStream.of(this)
345344
} else {
346345
return IStream.of(child2, child1)
347346
.flatMapOrdered { it.resolve() }
348-
.flatMapOrdered { it.data.splitAtInterval(interval, timeRangeFilter) }
347+
.flatMapOrdered { it.data.splitAtInterval(intervalSpec) }
349348
}
350349
}
351350

@@ -396,7 +395,7 @@ data class HistoryIndexRangeNode(
396395
}
397396

398397
private fun LongRange.shift(amount: Long) = first.plus(amount)..last.plus(amount)
399-
private fun <T : Comparable<T>> ClosedRange<T>.intersects(other: ClosedRange<T>): Boolean {
398+
fun <T : Comparable<T>> ClosedRange<T>.intersects(other: ClosedRange<T>): Boolean {
400399
return this.contains(other.start) || this.contains(other.endInclusive) ||
401400
other.contains(this.start) || other.contains(this.endInclusive)
402401
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.modelix.datastructures.history
2+
3+
import kotlinx.datetime.Instant
4+
import org.modelix.datastructures.objects.ObjectHash
5+
6+
/**
7+
* A summary of a range of versions.
8+
*/
9+
data class HistoryInterval(
10+
val firstVersionHash: ObjectHash,
11+
val lastVersionHash: ObjectHash,
12+
/**
13+
* Number of versions contained in this interval.
14+
*/
15+
val size: Long,
16+
val minTime: Instant,
17+
val maxTime: Instant,
18+
val authors: Set<String>,
19+
)

0 commit comments

Comments
 (0)