Add lyng.observable ObservableList hooks, flow events, and docs
This commit is contained in:
parent
a9da21e7a4
commit
298c2a09fe
@ -11,6 +11,7 @@ Concrete collection classes:
|
|||||||
|
|
||||||
- Mutable: [List], [Set], [Map]
|
- Mutable: [List], [Set], [Map]
|
||||||
- Immutable: [ImmutableList], [ImmutableSet], [ImmutableMap]
|
- Immutable: [ImmutableList], [ImmutableSet], [ImmutableMap]
|
||||||
|
- Observable mutable lists (opt-in module): [ObservableList]
|
||||||
|
|
||||||
| name | description |
|
| name | description |
|
||||||
|------------------------|------------------------------------------------------|
|
|------------------------|------------------------------------------------------|
|
||||||
@ -27,3 +28,4 @@ See [List], [Set], [Iterable] and [Efficient Iterables in Kotlin Interop](Effici
|
|||||||
[ImmutableList]: ImmutableList.md
|
[ImmutableList]: ImmutableList.md
|
||||||
[ImmutableSet]: ImmutableSet.md
|
[ImmutableSet]: ImmutableSet.md
|
||||||
[ImmutableMap]: ImmutableMap.md
|
[ImmutableMap]: ImmutableMap.md
|
||||||
|
[ObservableList]: ObservableList.md
|
||||||
|
|||||||
46
docs/List.md
46
docs/List.md
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
Mutable list of any objects.
|
Mutable list of any objects.
|
||||||
For immutable list values, see [ImmutableList].
|
For immutable list values, see [ImmutableList].
|
||||||
|
For observable mutable lists and change hooks, see [ObservableList].
|
||||||
|
|
||||||
It's class in Lyng is `List`:
|
It's class in Lyng is `List`:
|
||||||
|
|
||||||
@ -180,6 +181,50 @@ for `sort()` will be `sort { a, b -> a <=> b }
|
|||||||
|
|
||||||
It inherits from [Iterable] too and thus all iterable methods are applicable to any list.
|
It inherits from [Iterable] too and thus all iterable methods are applicable to any list.
|
||||||
|
|
||||||
|
## Observable list hooks
|
||||||
|
|
||||||
|
Observable hooks are provided by module `lyng.observable` and are opt-in:
|
||||||
|
|
||||||
|
import lyng.observable
|
||||||
|
|
||||||
|
val src = [1,2,3]
|
||||||
|
val xs = src.observable()
|
||||||
|
assert(xs is ObservableList<Int>)
|
||||||
|
|
||||||
|
var before = 0
|
||||||
|
var after = 0
|
||||||
|
xs.beforeChange { before++ }
|
||||||
|
xs.onChange { after++ }
|
||||||
|
|
||||||
|
xs += 4
|
||||||
|
xs[0] = 100
|
||||||
|
assertEquals([100,2,3,4], xs)
|
||||||
|
assertEquals(2, before)
|
||||||
|
assertEquals(2, after)
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
`beforeChange` runs before mutation commit and may reject it by throwing exception (typically `ChangeRejectionException` from the same module):
|
||||||
|
|
||||||
|
import lyng.observable
|
||||||
|
val xs = [1,2].observable()
|
||||||
|
xs.beforeChange { throw ChangeRejectionException("read only") }
|
||||||
|
assertThrows(ChangeRejectionException) { xs += 3 }
|
||||||
|
assertEquals([1,2], xs)
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
`changes()` returns `Flow<ListChange<T>>` of committed events:
|
||||||
|
|
||||||
|
import lyng.observable
|
||||||
|
val xs = [10,20].observable()
|
||||||
|
val it = xs.changes().iterator()
|
||||||
|
xs += 30
|
||||||
|
assert(it.hasNext())
|
||||||
|
val e = it.next()
|
||||||
|
assert(e is ListInsert<Int>)
|
||||||
|
assertEquals([30], (e as ListInsert<Int>).values)
|
||||||
|
it.cancelIteration()
|
||||||
|
>>> void
|
||||||
|
|
||||||
## Member inherited from Array
|
## Member inherited from Array
|
||||||
|
|
||||||
| name | meaning | type |
|
| name | meaning | type |
|
||||||
@ -199,3 +244,4 @@ It inherits from [Iterable] too and thus all iterable methods are applicable to
|
|||||||
|
|
||||||
[Iterable]: Iterable.md
|
[Iterable]: Iterable.md
|
||||||
[ImmutableList]: ImmutableList.md
|
[ImmutableList]: ImmutableList.md
|
||||||
|
[ObservableList]: ObservableList.md
|
||||||
|
|||||||
70
docs/ObservableList.md
Normal file
70
docs/ObservableList.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# ObservableList module
|
||||||
|
|
||||||
|
`ObservableList` lives in explicit module `lyng.observable`.
|
||||||
|
|
||||||
|
Import it first:
|
||||||
|
|
||||||
|
import lyng.observable
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Create from a regular mutable list:
|
||||||
|
|
||||||
|
import lyng.observable
|
||||||
|
val xs = [1,2,3].observable()
|
||||||
|
assert(xs is ObservableList<Int>)
|
||||||
|
assertEquals([1,2,3], xs)
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
## Hook flow
|
||||||
|
|
||||||
|
Event order is:
|
||||||
|
1. `beforeChange(change)` listeners
|
||||||
|
2. mutation commit
|
||||||
|
3. `onChange(change)` listeners
|
||||||
|
4. `changes()` flow emission
|
||||||
|
|
||||||
|
Rejection is done by throwing in `beforeChange`.
|
||||||
|
|
||||||
|
import lyng.observable
|
||||||
|
val xs = [1,2].observable()
|
||||||
|
xs.beforeChange {
|
||||||
|
throw ChangeRejectionException("no mutation")
|
||||||
|
}
|
||||||
|
assertThrows(ChangeRejectionException) { xs += 3 }
|
||||||
|
assertEquals([1,2], xs)
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
## Subscriptions
|
||||||
|
|
||||||
|
`beforeChange` and `onChange` return `Subscription`.
|
||||||
|
Call `cancel()` to unsubscribe.
|
||||||
|
|
||||||
|
import lyng.observable
|
||||||
|
val xs = [1].observable()
|
||||||
|
var hits = 0
|
||||||
|
val sub = xs.onChange { hits++ }
|
||||||
|
xs += 2
|
||||||
|
sub.cancel()
|
||||||
|
xs += 3
|
||||||
|
assertEquals(1, hits)
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
## Change events
|
||||||
|
|
||||||
|
`changes()` returns `Flow<ListChange<T>>` with concrete event classes:
|
||||||
|
- `ListInsert`
|
||||||
|
- `ListSet`
|
||||||
|
- `ListRemove`
|
||||||
|
- `ListClear`
|
||||||
|
- `ListReorder`
|
||||||
|
|
||||||
|
import lyng.observable
|
||||||
|
val xs = [10,20].observable()
|
||||||
|
val it = xs.changes().iterator()
|
||||||
|
xs[1] = 200
|
||||||
|
val ev = it.next()
|
||||||
|
assert(ev is ListSet<Int>)
|
||||||
|
assertEquals(20, (ev as ListSet<Int>).oldValue)
|
||||||
|
assertEquals(200, ev.newValue)
|
||||||
|
it.cancelIteration()
|
||||||
|
>>> void
|
||||||
@ -4704,6 +4704,22 @@ class Compiler(
|
|||||||
name == "toImmutableMap" && receiver is TypeDecl.Generic && base == "Iterable" -> {
|
name == "toImmutableMap" && receiver is TypeDecl.Generic && base == "Iterable" -> {
|
||||||
TypeDecl.Generic("ImmutableMap", listOf(TypeDecl.TypeAny, TypeDecl.TypeAny), false)
|
TypeDecl.Generic("ImmutableMap", listOf(TypeDecl.TypeAny, TypeDecl.TypeAny), false)
|
||||||
}
|
}
|
||||||
|
name == "observable" && receiver is TypeDecl.Generic && (base == "List" || base == "ObservableList") -> {
|
||||||
|
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
|
||||||
|
TypeDecl.Generic("ObservableList", listOf(arg), false)
|
||||||
|
}
|
||||||
|
name == "changes" && receiver is TypeDecl.Generic && base == "ObservableList" -> {
|
||||||
|
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
|
||||||
|
TypeDecl.Generic("Flow", listOf(TypeDecl.Generic("ListChange", listOf(arg), false)), false)
|
||||||
|
}
|
||||||
|
name == "changes" && receiver is TypeDecl.Generic && base == "Observable" -> {
|
||||||
|
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
|
||||||
|
TypeDecl.Generic("Flow", listOf(arg), false)
|
||||||
|
}
|
||||||
|
(name == "beforeChange" || name == "onChange") && receiver is TypeDecl.Generic &&
|
||||||
|
(base == "Observable" || base == "ObservableList") -> {
|
||||||
|
TypeDecl.Simple("Subscription", false)
|
||||||
|
}
|
||||||
name == "toMap" && receiver is TypeDecl.Generic && base == "Iterable" -> {
|
name == "toMap" && receiver is TypeDecl.Generic && base == "Iterable" -> {
|
||||||
TypeDecl.Generic("Map", listOf(TypeDecl.TypeAny, TypeDecl.TypeAny), false)
|
TypeDecl.Generic("Map", listOf(TypeDecl.TypeAny, TypeDecl.TypeAny), false)
|
||||||
}
|
}
|
||||||
@ -4794,6 +4810,9 @@ class Compiler(
|
|||||||
"toImmutableList" -> ObjImmutableList.type
|
"toImmutableList" -> ObjImmutableList.type
|
||||||
"toImmutableSet" -> ObjImmutableSet.type
|
"toImmutableSet" -> ObjImmutableSet.type
|
||||||
"toImmutableMap" -> ObjImmutableMap.type
|
"toImmutableMap" -> ObjImmutableMap.type
|
||||||
|
"observable" -> ObjObservableList.type
|
||||||
|
"beforeChange", "onChange" -> ObjSubscription.type
|
||||||
|
"changes" -> ObjFlow.type
|
||||||
"toImmutable" -> Obj.rootObjectType
|
"toImmutable" -> Obj.rootObjectType
|
||||||
"toMutable" -> ObjMutableBuffer.type
|
"toMutable" -> ObjMutableBuffer.type
|
||||||
"seq" -> ObjFlow.type
|
"seq" -> ObjFlow.type
|
||||||
@ -8322,6 +8341,16 @@ class Compiler(
|
|||||||
"Regex" -> ObjRegex.type
|
"Regex" -> ObjRegex.type
|
||||||
"RegexMatch" -> ObjRegexMatch.type
|
"RegexMatch" -> ObjRegexMatch.type
|
||||||
"MapEntry" -> ObjMapEntry.type
|
"MapEntry" -> ObjMapEntry.type
|
||||||
|
"Observable" -> ObjObservable
|
||||||
|
"Subscription" -> ObjSubscription.type
|
||||||
|
"ListChange" -> ObjListChange.type
|
||||||
|
"ListSet" -> ObjListSetChange.type
|
||||||
|
"ListInsert" -> ObjListInsertChange.type
|
||||||
|
"ListRemove" -> ObjListRemoveChange.type
|
||||||
|
"ListClear" -> ObjListClearChange.type
|
||||||
|
"ListReorder" -> ObjListReorderChange.type
|
||||||
|
"ObservableList" -> ObjObservableList.type
|
||||||
|
"ChangeRejectionException" -> ObjChangeRejectionExceptionClass
|
||||||
"Exception" -> ObjException.Root
|
"Exception" -> ObjException.Root
|
||||||
"Instant" -> ObjInstant.type
|
"Instant" -> ObjInstant.type
|
||||||
"DateTime" -> ObjDateTime.type
|
"DateTime" -> ObjDateTime.type
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import net.sergeych.lyng.bytecode.CmdVm
|
|||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
import net.sergeych.lyng.pacman.ImportManager
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyng.stdlib_included.observableLyng
|
||||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||||
import net.sergeych.lynon.ObjLynonClass
|
import net.sergeych.lynon.ObjLynonClass
|
||||||
import net.sergeych.mp_tools.globalDefer
|
import net.sergeych.mp_tools.globalDefer
|
||||||
@ -593,6 +594,19 @@ class Script(
|
|||||||
module.eval(Source("lyng.stdlib", rootLyng))
|
module.eval(Source("lyng.stdlib", rootLyng))
|
||||||
ObjKotlinIterator.bindTo(module.requireClass("KotlinIterator"))
|
ObjKotlinIterator.bindTo(module.requireClass("KotlinIterator"))
|
||||||
}
|
}
|
||||||
|
addPackage("lyng.observable") { module ->
|
||||||
|
module.addConst("Observable", ObjObservable)
|
||||||
|
module.addConst("Subscription", ObjSubscription.type)
|
||||||
|
module.addConst("ListChange", ObjListChange.type)
|
||||||
|
module.addConst("ListSet", ObjListSetChange.type)
|
||||||
|
module.addConst("ListInsert", ObjListInsertChange.type)
|
||||||
|
module.addConst("ListRemove", ObjListRemoveChange.type)
|
||||||
|
module.addConst("ListClear", ObjListClearChange.type)
|
||||||
|
module.addConst("ListReorder", ObjListReorderChange.type)
|
||||||
|
module.addConst("ObservableList", ObjObservableList.type)
|
||||||
|
module.addConst("ChangeRejectionException", ObjChangeRejectionExceptionClass)
|
||||||
|
module.eval(Source("lyng.observable", observableLyng))
|
||||||
|
}
|
||||||
addPackage("lyng.buffer") {
|
addPackage("lyng.buffer") {
|
||||||
it.addConstDoc(
|
it.addConstDoc(
|
||||||
name = "Buffer",
|
name = "Buffer",
|
||||||
|
|||||||
@ -4440,6 +4440,7 @@ class BytecodeCompiler(
|
|||||||
val initClass = when (localTarget?.name) {
|
val initClass = when (localTarget?.name) {
|
||||||
"List" -> ObjList.type
|
"List" -> ObjList.type
|
||||||
"ImmutableList" -> ObjImmutableList.type
|
"ImmutableList" -> ObjImmutableList.type
|
||||||
|
"ObservableList" -> ObjObservableList.type
|
||||||
"Map" -> ObjMap.type
|
"Map" -> ObjMap.type
|
||||||
"ImmutableMap" -> ObjImmutableMap.type
|
"ImmutableMap" -> ObjImmutableMap.type
|
||||||
"Set" -> ObjSet.type
|
"Set" -> ObjSet.type
|
||||||
@ -7326,6 +7327,16 @@ class BytecodeCompiler(
|
|||||||
"Regex" -> ObjRegex.type
|
"Regex" -> ObjRegex.type
|
||||||
"RegexMatch" -> ObjRegexMatch.type
|
"RegexMatch" -> ObjRegexMatch.type
|
||||||
"MapEntry" -> ObjMapEntry.type
|
"MapEntry" -> ObjMapEntry.type
|
||||||
|
"Observable" -> ObjObservable
|
||||||
|
"Subscription" -> ObjSubscription.type
|
||||||
|
"ListChange" -> ObjListChange.type
|
||||||
|
"ListSet" -> ObjListSetChange.type
|
||||||
|
"ListInsert" -> ObjListInsertChange.type
|
||||||
|
"ListRemove" -> ObjListRemoveChange.type
|
||||||
|
"ListClear" -> ObjListClearChange.type
|
||||||
|
"ListReorder" -> ObjListReorderChange.type
|
||||||
|
"ObservableList" -> ObjObservableList.type
|
||||||
|
"ChangeRejectionException" -> ObjChangeRejectionExceptionClass
|
||||||
"Instant" -> ObjInstant.type
|
"Instant" -> ObjInstant.type
|
||||||
"DateTime" -> ObjDateTime.type
|
"DateTime" -> ObjDateTime.type
|
||||||
"Duration" -> ObjDuration.type
|
"Duration" -> ObjDuration.type
|
||||||
@ -7389,6 +7400,9 @@ class BytecodeCompiler(
|
|||||||
"toImmutableSet" -> ObjImmutableSet.type
|
"toImmutableSet" -> ObjImmutableSet.type
|
||||||
"toMap" -> ObjMap.type
|
"toMap" -> ObjMap.type
|
||||||
"toImmutableMap" -> ObjImmutableMap.type
|
"toImmutableMap" -> ObjImmutableMap.type
|
||||||
|
"observable" -> ObjObservableList.type
|
||||||
|
"beforeChange", "onChange" -> ObjSubscription.type
|
||||||
|
"changes" -> ObjFlow.type
|
||||||
"joinToString" -> ObjString.type
|
"joinToString" -> ObjString.type
|
||||||
"now",
|
"now",
|
||||||
"truncateToSecond",
|
"truncateToSecond",
|
||||||
|
|||||||
@ -47,6 +47,10 @@ object StdlibDocsBootstrap {
|
|||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
val _immutableSet = net.sergeych.lyng.obj.ObjImmutableSet.type
|
val _immutableSet = net.sergeych.lyng.obj.ObjImmutableSet.type
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _observableList = net.sergeych.lyng.obj.ObjObservableList.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _subscription = net.sergeych.lyng.obj.ObjSubscription.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
val _int = net.sergeych.lyng.obj.ObjInt.type
|
val _int = net.sergeych.lyng.obj.ObjInt.type
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
val _real = net.sergeych.lyng.obj.ObjReal.type
|
val _real = net.sergeych.lyng.obj.ObjReal.type
|
||||||
|
|||||||
@ -29,8 +29,8 @@ import net.sergeych.lynon.LynonDecoder
|
|||||||
import net.sergeych.lynon.LynonEncoder
|
import net.sergeych.lynon.LynonEncoder
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
|
|
||||||
class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
open class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||||
private fun shouldTreatAsSingleElement(scope: Scope, other: Obj): Boolean {
|
protected open fun shouldTreatAsSingleElement(scope: Scope, other: Obj): Boolean {
|
||||||
if (!other.isInstanceOf(ObjIterable)) return true
|
if (!other.isInstanceOf(ObjIterable)) return true
|
||||||
val declaredElementType = scope.declaredListElementTypeForValue(this)
|
val declaredElementType = scope.declaredListElementTypeForValue(this)
|
||||||
if (declaredElementType != null && matchesTypeDecl(scope, other, declaredElementType)) {
|
if (declaredElementType != null && matchesTypeDecl(scope, other, declaredElementType)) {
|
||||||
@ -95,7 +95,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
open override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||||
list[index.toInt()] = newValue
|
list[index.toInt()] = newValue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +149,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
|
open override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
|
||||||
if (other is ObjList) {
|
if (other is ObjList) {
|
||||||
list.addAll(other.list)
|
list.addAll(other.list)
|
||||||
} else if (!shouldTreatAsSingleElement(scope, other) && other.isInstanceOf(ObjIterable)) {
|
} else if (!shouldTreatAsSingleElement(scope, other) && other.isInstanceOf(ObjIterable)) {
|
||||||
@ -180,7 +180,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
|||||||
return ObjList(out)
|
return ObjList(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun minusAssign(scope: Scope, other: Obj): Obj {
|
open override suspend fun minusAssign(scope: Scope, other: Obj): Obj {
|
||||||
if (shouldTreatAsSingleElement(scope, other)) {
|
if (shouldTreatAsSingleElement(scope, other)) {
|
||||||
list.remove(other)
|
list.remove(other)
|
||||||
return this
|
return this
|
||||||
@ -221,7 +221,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val objClass: ObjClass
|
open override val objClass: ObjClass
|
||||||
get() = type
|
get() = type
|
||||||
|
|
||||||
override suspend fun toKotlin(scope: Scope): Any {
|
override suspend fun toKotlin(scope: Scope): Any {
|
||||||
|
|||||||
@ -0,0 +1,589 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import net.sergeych.lyng.Arguments
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.asFacade
|
||||||
|
import net.sergeych.lyng.miniast.ParamDoc
|
||||||
|
import net.sergeych.lyng.miniast.TypeGenericDoc
|
||||||
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
|
import net.sergeych.lyng.miniast.addPropertyDoc
|
||||||
|
import net.sergeych.lyng.miniast.type
|
||||||
|
|
||||||
|
val ObjObservable = ObjClass("Observable").apply {
|
||||||
|
addFn("beforeChange") { raiseNotImplemented("Observable.beforeChange") }
|
||||||
|
addFn("onChange") { raiseNotImplemented("Observable.onChange") }
|
||||||
|
addFn("changes") { raiseNotImplemented("Observable.changes") }
|
||||||
|
}
|
||||||
|
|
||||||
|
val ObjChangeRejectionExceptionClass = ObjException.Companion.ExceptionClass("ChangeRejectionException", ObjException.Root)
|
||||||
|
|
||||||
|
open class ObjListChange : Obj() {
|
||||||
|
override val objClass: ObjClass get() = type
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = ObjClass("ListChange")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjListSetChange(val index: Int, val oldValue: Obj, val newValue: Obj) : ObjListChange() {
|
||||||
|
override val objClass: ObjClass get() = type
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = ObjClass("ListSet", ObjListChange.type).apply {
|
||||||
|
addPropertyDoc("index", "Changed index.", type("lyng.Int"), moduleName = "lyng.observable", getter = { thisAs<ObjListSetChange>().index.toObj() })
|
||||||
|
addPropertyDoc("oldValue", "Value before assignment.", type("lyng.Any"), moduleName = "lyng.observable", getter = { thisAs<ObjListSetChange>().oldValue })
|
||||||
|
addPropertyDoc("newValue", "Assigned value.", type("lyng.Any"), moduleName = "lyng.observable", getter = { thisAs<ObjListSetChange>().newValue })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjListInsertChange(val index: Int, val values: ObjList) : ObjListChange() {
|
||||||
|
override val objClass: ObjClass get() = type
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = ObjClass("ListInsert", ObjListChange.type).apply {
|
||||||
|
addPropertyDoc("index", "Insertion index.", type("lyng.Int"), moduleName = "lyng.observable", getter = { thisAs<ObjListInsertChange>().index.toObj() })
|
||||||
|
addPropertyDoc(
|
||||||
|
"values",
|
||||||
|
"Inserted values.",
|
||||||
|
TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))),
|
||||||
|
moduleName = "lyng.observable",
|
||||||
|
getter = { thisAs<ObjListInsertChange>().values }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjListRemoveChange(val index: Int, val oldValue: Obj) : ObjListChange() {
|
||||||
|
override val objClass: ObjClass get() = type
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = ObjClass("ListRemove", ObjListChange.type).apply {
|
||||||
|
addPropertyDoc("index", "Removed index.", type("lyng.Int"), moduleName = "lyng.observable", getter = { thisAs<ObjListRemoveChange>().index.toObj() })
|
||||||
|
addPropertyDoc("oldValue", "Removed value.", type("lyng.Any"), moduleName = "lyng.observable", getter = { thisAs<ObjListRemoveChange>().oldValue })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjListClearChange(val oldValues: ObjList) : ObjListChange() {
|
||||||
|
override val objClass: ObjClass get() = type
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = ObjClass("ListClear", ObjListChange.type).apply {
|
||||||
|
addPropertyDoc(
|
||||||
|
"oldValues",
|
||||||
|
"All list values before clear.",
|
||||||
|
TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))),
|
||||||
|
moduleName = "lyng.observable",
|
||||||
|
getter = { thisAs<ObjListClearChange>().oldValues }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjListReorderChange(val oldValues: ObjList, val newValues: Obj) : ObjListChange() {
|
||||||
|
override val objClass: ObjClass get() = type
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = ObjClass("ListReorder", ObjListChange.type).apply {
|
||||||
|
addPropertyDoc(
|
||||||
|
"oldValues",
|
||||||
|
"Values before reorder.",
|
||||||
|
TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))),
|
||||||
|
moduleName = "lyng.observable",
|
||||||
|
getter = { thisAs<ObjListReorderChange>().oldValues }
|
||||||
|
)
|
||||||
|
addPropertyDoc(
|
||||||
|
"newValues",
|
||||||
|
"Values after reorder (or Unset in beforeChange when unknown before commit).",
|
||||||
|
type("lyng.Any"),
|
||||||
|
moduleName = "lyng.observable",
|
||||||
|
getter = { thisAs<ObjListReorderChange>().newValues }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjSubscription(private val onCancel: suspend () -> Unit) : Obj() {
|
||||||
|
private val access = Mutex()
|
||||||
|
private var cancelled = false
|
||||||
|
|
||||||
|
override val objClass: ObjClass get() = type
|
||||||
|
|
||||||
|
suspend fun cancel() {
|
||||||
|
var action: (suspend () -> Unit)? = null
|
||||||
|
access.withLock {
|
||||||
|
if (!cancelled) {
|
||||||
|
cancelled = true
|
||||||
|
action = onCancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = object : ObjClass("Subscription") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj = scope.raiseIllegalArgument("Subscription constructor is not available")
|
||||||
|
}.apply {
|
||||||
|
addFnDoc(
|
||||||
|
"cancel",
|
||||||
|
"Unsubscribe listener. Safe to call multiple times.",
|
||||||
|
returns = type("lyng.Void"),
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
thisAs<ObjSubscription>().cancel()
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjObservableListFlowProducer(
|
||||||
|
private val owner: ObjObservableList,
|
||||||
|
private val subscriptionId: Long,
|
||||||
|
private val channel: ReceiveChannel<Obj>
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass get() = Obj.rootObjectType
|
||||||
|
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
val builder = scope.thisObj as? ObjFlowBuilder
|
||||||
|
?: scope.raiseIllegalState("flow builder is not available")
|
||||||
|
try {
|
||||||
|
for (change in channel) {
|
||||||
|
builder.invokeInstanceMethod(scope, "emit", change)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
owner.unsubscribeChangeChannel(subscriptionId)
|
||||||
|
}
|
||||||
|
return ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjObservableList(list: MutableList<Obj> = mutableListOf()) : ObjList(list) {
|
||||||
|
private val access = Mutex()
|
||||||
|
private var nextSubscriptionId: Long = 1L
|
||||||
|
private val beforeListeners = linkedMapOf<Long, Obj>()
|
||||||
|
private val afterListeners = linkedMapOf<Long, Obj>()
|
||||||
|
private val changeChannels = linkedMapOf<Long, Channel<Obj>>()
|
||||||
|
|
||||||
|
override val objClass: ObjClass get() = type
|
||||||
|
|
||||||
|
private fun nextId(): Long = nextSubscriptionId++
|
||||||
|
|
||||||
|
private suspend fun addBeforeListener(listener: Obj): ObjSubscription {
|
||||||
|
val id = access.withLock {
|
||||||
|
val id = nextId()
|
||||||
|
beforeListeners[id] = listener
|
||||||
|
id
|
||||||
|
}
|
||||||
|
return ObjSubscription {
|
||||||
|
access.withLock { beforeListeners.remove(id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun addAfterListener(listener: Obj): ObjSubscription {
|
||||||
|
val id = access.withLock {
|
||||||
|
val id = nextId()
|
||||||
|
afterListeners[id] = listener
|
||||||
|
id
|
||||||
|
}
|
||||||
|
return ObjSubscription {
|
||||||
|
access.withLock { afterListeners.remove(id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun notifyListeners(scope: Scope, listeners: Collection<Obj>, change: Obj) {
|
||||||
|
val facade = scope.asFacade()
|
||||||
|
for (listener in listeners) {
|
||||||
|
facade.call(listener, Arguments(change))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun snapshotBeforeListeners(): List<Obj> = access.withLock { beforeListeners.values.toList() }
|
||||||
|
private suspend fun snapshotAfterListeners(): List<Obj> = access.withLock { afterListeners.values.toList() }
|
||||||
|
|
||||||
|
private suspend fun notifyBefore(scope: Scope, change: Obj) {
|
||||||
|
notifyListeners(scope, snapshotBeforeListeners(), change)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun notifyAfter(scope: Scope, change: Obj) {
|
||||||
|
notifyListeners(scope, snapshotAfterListeners(), change)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitCommittedChange(change: Obj) {
|
||||||
|
val channels = access.withLock { changeChannels.toMap() }
|
||||||
|
val dead = mutableListOf<Long>()
|
||||||
|
for ((id, channel) in channels) {
|
||||||
|
if (channel.trySend(change).isFailure) {
|
||||||
|
dead += id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dead.isNotEmpty()) {
|
||||||
|
access.withLock {
|
||||||
|
for (id in dead) {
|
||||||
|
changeChannels.remove(id)?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mutate(scope: Scope, beforeChange: Obj, mutation: suspend () -> Obj?): Obj? {
|
||||||
|
notifyBefore(scope, beforeChange)
|
||||||
|
val committed = mutation()
|
||||||
|
if (committed != null) {
|
||||||
|
notifyAfter(scope, committed)
|
||||||
|
emitCommittedChange(committed)
|
||||||
|
}
|
||||||
|
return committed
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun snapshotList(): ObjList = ObjList(list.toMutableList())
|
||||||
|
|
||||||
|
private suspend fun createChangesFlow(scope: Scope): ObjFlow {
|
||||||
|
val channel = Channel<Obj>(Channel.UNLIMITED)
|
||||||
|
val id = access.withLock {
|
||||||
|
val id = nextId()
|
||||||
|
changeChannels[id] = channel
|
||||||
|
id
|
||||||
|
}
|
||||||
|
val producer = ObjObservableListFlowProducer(this, id, channel)
|
||||||
|
return ObjFlow(producer, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal suspend fun unsubscribeChangeChannel(id: Long) {
|
||||||
|
access.withLock {
|
||||||
|
changeChannels.remove(id)?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||||
|
val idx = index.toInt()
|
||||||
|
val old = list[idx]
|
||||||
|
val change = ObjListSetChange(idx, old, newValue)
|
||||||
|
mutate(scope, change) {
|
||||||
|
list[idx] = newValue
|
||||||
|
change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
|
||||||
|
when {
|
||||||
|
other is ObjList -> {
|
||||||
|
if (other.list.isEmpty()) return this
|
||||||
|
val insert = ObjListInsertChange(list.size, ObjList(other.list.toMutableList()))
|
||||||
|
mutate(scope, insert) {
|
||||||
|
list.addAll(other.list)
|
||||||
|
insert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!shouldTreatAsSingleElement(scope, other) && other.isInstanceOf(ObjIterable) -> {
|
||||||
|
val otherList = (other.invokeInstanceMethod(scope, "toList") as ObjList).list
|
||||||
|
if (otherList.isEmpty()) return this
|
||||||
|
val insert = ObjListInsertChange(list.size, ObjList(otherList.toMutableList()))
|
||||||
|
mutate(scope, insert) {
|
||||||
|
list.addAll(otherList)
|
||||||
|
insert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val insert = ObjListInsertChange(list.size, ObjList(mutableListOf(other)))
|
||||||
|
mutate(scope, insert) {
|
||||||
|
list.add(other)
|
||||||
|
insert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun minusAssign(scope: Scope, other: Obj): Obj {
|
||||||
|
if (shouldTreatAsSingleElement(scope, other)) {
|
||||||
|
val index = list.indexOf(other)
|
||||||
|
if (index < 0) return this
|
||||||
|
val old = list[index]
|
||||||
|
val remove = ObjListRemoveChange(index, old)
|
||||||
|
mutate(scope, remove) {
|
||||||
|
list.removeAt(index)
|
||||||
|
remove
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
if (other.isInstanceOf(ObjIterable)) {
|
||||||
|
val toRemove = mutableSetOf<Obj>()
|
||||||
|
other.enumerate(scope) {
|
||||||
|
toRemove += it
|
||||||
|
true
|
||||||
|
}
|
||||||
|
val oldValues = snapshotList()
|
||||||
|
val filtered = list.filterNot { toRemove.contains(it) }
|
||||||
|
if (filtered.size == list.size) return this
|
||||||
|
val committed = if (filtered.isEmpty()) {
|
||||||
|
ObjListClearChange(oldValues)
|
||||||
|
} else {
|
||||||
|
ObjListReorderChange(oldValues, ObjList(filtered.toMutableList()))
|
||||||
|
}
|
||||||
|
mutate(scope, committed) {
|
||||||
|
list.clear()
|
||||||
|
list.addAll(filtered)
|
||||||
|
committed
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
val index = list.indexOf(other)
|
||||||
|
if (index < 0) return this
|
||||||
|
val old = list[index]
|
||||||
|
val remove = ObjListRemoveChange(index, old)
|
||||||
|
mutate(scope, remove) {
|
||||||
|
list.removeAt(index)
|
||||||
|
remove
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = object : ObjClass("ObservableList", ObjList.type) {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
return ObjObservableList(scope.args.list.toMutableList())
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addFnDoc(
|
||||||
|
"beforeChange",
|
||||||
|
"Subscribe to pre-commit list changes. Throw to reject mutation.",
|
||||||
|
params = listOf(ParamDoc("listener", type("lyng.Any"))),
|
||||||
|
returns = type("lyng.observable.Subscription"),
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
thisAs<ObjObservableList>().addBeforeListener(requireOnlyArg())
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
"onChange",
|
||||||
|
"Subscribe to post-commit list changes.",
|
||||||
|
params = listOf(ParamDoc("listener", type("lyng.Any"))),
|
||||||
|
returns = type("lyng.observable.Subscription"),
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
thisAs<ObjObservableList>().addAfterListener(requireOnlyArg())
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
"changes",
|
||||||
|
"Get a flow of committed list changes.",
|
||||||
|
returns = TypeGenericDoc(type("lyng.Flow"), listOf(type("lyng.observable.ListChange"))),
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
thisAs<ObjObservableList>().createChangesFlow(requireScope())
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
"observable",
|
||||||
|
"Observable list is already observable; returns this.",
|
||||||
|
returns = type("lyng.observable.ObservableList"),
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) { thisObj }
|
||||||
|
addFnDoc(
|
||||||
|
"add",
|
||||||
|
"Append one or more elements and emit ListInsert.",
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
val self = thisAs<ObjObservableList>()
|
||||||
|
if (args.isEmpty()) return@addFnDoc ObjVoid
|
||||||
|
val inserted = ObjList(args.list.toMutableList())
|
||||||
|
val change = ObjListInsertChange(self.list.size, inserted)
|
||||||
|
self.mutate(requireScope(), change) {
|
||||||
|
self.list.addAll(args.list)
|
||||||
|
change
|
||||||
|
}
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
"insertAt",
|
||||||
|
"Insert one or more values at index and emit ListInsert.",
|
||||||
|
params = listOf(ParamDoc("index", type("lyng.Int"))),
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
if (args.size < 2) raiseError("insertAt takes 2+ arguments")
|
||||||
|
val self = thisAs<ObjObservableList>()
|
||||||
|
val index = requiredArg<ObjInt>(0).value.toInt()
|
||||||
|
val values = args.list.drop(1)
|
||||||
|
val change = ObjListInsertChange(index, ObjList(values.toMutableList()))
|
||||||
|
self.mutate(requireScope(), change) {
|
||||||
|
var i = index
|
||||||
|
for (v in values) self.list.add(i++, v)
|
||||||
|
change
|
||||||
|
}
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
"removeAt",
|
||||||
|
"Remove element at index or range and emit remove/reorder/clear changes.",
|
||||||
|
params = listOf(ParamDoc("start", type("lyng.Int")), ParamDoc("end", type("lyng.Int"))),
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
val self = thisAs<ObjObservableList>()
|
||||||
|
val start = requiredArg<ObjInt>(0).value.toInt()
|
||||||
|
if (args.size == 2) {
|
||||||
|
val end = requireOnlyArg<ObjInt>().value.toInt()
|
||||||
|
val oldValues = self.snapshotList()
|
||||||
|
val newValues = self.list.toMutableList().apply { subList(start, end).clear() }
|
||||||
|
if (newValues == self.list) return@addFnDoc self
|
||||||
|
val change = if (newValues.isEmpty()) ObjListClearChange(oldValues)
|
||||||
|
else ObjListReorderChange(oldValues, ObjList(newValues.toMutableList()))
|
||||||
|
self.mutate(requireScope(), change) {
|
||||||
|
self.list.subList(start, end).clear()
|
||||||
|
change
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val old = self.list[start]
|
||||||
|
val change = ObjListRemoveChange(start, old)
|
||||||
|
self.mutate(requireScope(), change) {
|
||||||
|
self.list.removeAt(start)
|
||||||
|
change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
"removeLast",
|
||||||
|
"Remove last element(s) and emit remove/reorder/clear changes.",
|
||||||
|
params = listOf(ParamDoc("count", type("lyng.Int"))),
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
val self = thisAs<ObjObservableList>()
|
||||||
|
if (self.list.isEmpty()) return@addFnDoc self
|
||||||
|
if (args.isNotEmpty()) {
|
||||||
|
val count = requireOnlyArg<ObjInt>().value.toInt()
|
||||||
|
if (count <= 0) return@addFnDoc self
|
||||||
|
val oldValues = self.snapshotList()
|
||||||
|
val newValues = if (count >= self.list.size) emptyList() else self.list.subList(0, self.list.size - count).toList()
|
||||||
|
val change = if (newValues.isEmpty()) ObjListClearChange(oldValues)
|
||||||
|
else ObjListReorderChange(oldValues, ObjList(newValues.toMutableList()))
|
||||||
|
self.mutate(requireScope(), change) {
|
||||||
|
if (count >= self.list.size) self.list.clear()
|
||||||
|
else self.list.subList(self.list.size - count, self.list.size).clear()
|
||||||
|
change
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val index = self.list.lastIndex
|
||||||
|
val old = self.list[index]
|
||||||
|
val change = ObjListRemoveChange(index, old)
|
||||||
|
self.mutate(requireScope(), change) {
|
||||||
|
self.list.removeAt(index)
|
||||||
|
change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
"removeRange",
|
||||||
|
"Remove a range and emit reorder/clear changes.",
|
||||||
|
params = listOf(ParamDoc("range")),
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
val self = thisAs<ObjObservableList>()
|
||||||
|
val before = self.snapshotList()
|
||||||
|
val copy = self.list.toMutableList()
|
||||||
|
val range = requiredArg<Obj>(0)
|
||||||
|
if (range is ObjRange) {
|
||||||
|
val index = range
|
||||||
|
when {
|
||||||
|
index.start is ObjInt && index.end is ObjInt -> {
|
||||||
|
if (index.isEndInclusive) copy.subList(index.start.toInt(), index.end.toInt() + 1)
|
||||||
|
else copy.subList(index.start.toInt(), index.end.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
index.isOpenStart && !index.isOpenEnd -> {
|
||||||
|
if (index.isEndInclusive) copy.subList(0, index.end!!.toInt() + 1)
|
||||||
|
else copy.subList(0, index.end!!.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
index.isOpenEnd && !index.isOpenStart -> {
|
||||||
|
copy.subList(index.start!!.toInt(), copy.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
index.isOpenStart && index.isOpenEnd -> {
|
||||||
|
copy
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw RuntimeException("Can't apply range for index: $index")
|
||||||
|
}
|
||||||
|
}.clear()
|
||||||
|
} else {
|
||||||
|
val start = range.toInt()
|
||||||
|
val end = requiredArg<ObjInt>(1).value.toInt() + 1
|
||||||
|
copy.subList(start, end).clear()
|
||||||
|
}
|
||||||
|
if (copy == self.list) return@addFnDoc self
|
||||||
|
val change = if (copy.isEmpty()) ObjListClearChange(before)
|
||||||
|
else ObjListReorderChange(before, ObjList(copy.toMutableList()))
|
||||||
|
self.mutate(requireScope(), change) {
|
||||||
|
self.list.clear()
|
||||||
|
self.list.addAll(copy)
|
||||||
|
change
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
"sortWith",
|
||||||
|
"Sort list in place and emit ListReorder.",
|
||||||
|
params = listOf(ParamDoc("comparator")),
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
val self = thisAs<ObjObservableList>()
|
||||||
|
if (self.list.size < 2) return@addFnDoc ObjVoid
|
||||||
|
val comparator = requireOnlyArg<Obj>()
|
||||||
|
val oldValues = self.snapshotList()
|
||||||
|
val beforeChange = ObjListReorderChange(oldValues, ObjUnset)
|
||||||
|
self.notifyBefore(requireScope(), beforeChange)
|
||||||
|
self.quicksort { a, b -> call(comparator, Arguments(a, b)).toInt() }
|
||||||
|
val newValues = self.snapshotList()
|
||||||
|
if (oldValues.list != newValues.list) {
|
||||||
|
val committed = ObjListReorderChange(oldValues, newValues)
|
||||||
|
self.notifyAfter(requireScope(), committed)
|
||||||
|
self.emitCommittedChange(committed)
|
||||||
|
}
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
"shuffle",
|
||||||
|
"Shuffle list in place and emit ListReorder.",
|
||||||
|
moduleName = "lyng.observable"
|
||||||
|
) {
|
||||||
|
val self = thisAs<ObjObservableList>()
|
||||||
|
if (self.list.size < 2) return@addFnDoc ObjVoid
|
||||||
|
val oldValues = self.snapshotList()
|
||||||
|
val beforeChange = ObjListReorderChange(oldValues, ObjUnset)
|
||||||
|
self.notifyBefore(requireScope(), beforeChange)
|
||||||
|
self.list.shuffle()
|
||||||
|
val newValues = self.snapshotList()
|
||||||
|
if (oldValues.list != newValues.list) {
|
||||||
|
val committed = ObjListReorderChange(oldValues, newValues)
|
||||||
|
self.notifyAfter(requireScope(), committed)
|
||||||
|
self.emitCommittedChange(committed)
|
||||||
|
}
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.stdlib_included
|
||||||
|
|
||||||
|
@Suppress("Unused", "MemberVisibilityCanBePrivate")
|
||||||
|
internal val observableLyng = """
|
||||||
|
package lyng.observable
|
||||||
|
|
||||||
|
extern class ChangeRejectionException : Exception
|
||||||
|
|
||||||
|
extern class Subscription {
|
||||||
|
fun cancel(): Void
|
||||||
|
}
|
||||||
|
|
||||||
|
extern class Observable<Change> {
|
||||||
|
fun beforeChange(listener: (Change)->Void): Subscription
|
||||||
|
fun onChange(listener: (Change)->Void): Subscription
|
||||||
|
fun changes(): Flow<Change>
|
||||||
|
}
|
||||||
|
|
||||||
|
extern class ListChange<T>
|
||||||
|
|
||||||
|
extern class ListSet<T> : ListChange<T> {
|
||||||
|
val index: Int
|
||||||
|
val oldValue: Object
|
||||||
|
val newValue: Object
|
||||||
|
}
|
||||||
|
|
||||||
|
extern class ListInsert<T> : ListChange<T> {
|
||||||
|
val index: Int
|
||||||
|
val values: List<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
extern class ListRemove<T> : ListChange<T> {
|
||||||
|
val index: Int
|
||||||
|
val oldValue: Object
|
||||||
|
}
|
||||||
|
|
||||||
|
extern class ListClear<T> : ListChange<T> {
|
||||||
|
val oldValues: List<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
extern class ListReorder<T> : ListChange<T> {
|
||||||
|
val oldValues: List<T>
|
||||||
|
val newValues: Object
|
||||||
|
}
|
||||||
|
|
||||||
|
extern class ObservableList<T> : List<T> {
|
||||||
|
fun beforeChange(listener: (ListChange<T>)->Void): Subscription
|
||||||
|
fun onChange(listener: (ListChange<T>)->Void): Subscription
|
||||||
|
fun changes(): Flow<ListChange<T>>
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<T>.observable(): ObservableList<T> {
|
||||||
|
if( this is ObservableList<T> ) this
|
||||||
|
else ObservableList(...this)
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
117
lynglib/src/commonTest/kotlin/ObservableListTest.kt
Normal file
117
lynglib/src/commonTest/kotlin/ObservableListTest.kt
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.eval
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class ObservableListTest {
|
||||||
|
@Test
|
||||||
|
fun observableHooksAndSubscriptionCancel() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
import lyng.observable
|
||||||
|
|
||||||
|
val src = [1,2]
|
||||||
|
val xs = src.observable()
|
||||||
|
assert(xs is ObservableList<Int>)
|
||||||
|
assert(src == [1,2])
|
||||||
|
assert(xs == [1,2])
|
||||||
|
|
||||||
|
var before = 0
|
||||||
|
var after = 0
|
||||||
|
val beforeSub = xs.beforeChange { before++ }
|
||||||
|
val afterSub = xs.onChange { after++ }
|
||||||
|
|
||||||
|
xs += 3
|
||||||
|
assertEquals([1,2,3], xs)
|
||||||
|
assertEquals(1, before)
|
||||||
|
assertEquals(1, after)
|
||||||
|
|
||||||
|
afterSub.cancel()
|
||||||
|
xs[0] = 100
|
||||||
|
assertEquals([100,2,3], xs)
|
||||||
|
assertEquals(2, before)
|
||||||
|
assertEquals(1, after)
|
||||||
|
|
||||||
|
beforeSub.cancel()
|
||||||
|
xs.removeAt(0)
|
||||||
|
assertEquals([2,3], xs)
|
||||||
|
assertEquals(2, before)
|
||||||
|
assertEquals(1, after)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observableBeforeChangeRejectsMutation() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
import lyng.observable
|
||||||
|
|
||||||
|
val xs = [1,2].observable()
|
||||||
|
var after = 0
|
||||||
|
xs.beforeChange {
|
||||||
|
throw ChangeRejectionException("no changes accepted")
|
||||||
|
}
|
||||||
|
xs.onChange { after++ }
|
||||||
|
|
||||||
|
assertThrows(ChangeRejectionException) {
|
||||||
|
xs += 3
|
||||||
|
}
|
||||||
|
assertEquals([1,2], xs)
|
||||||
|
assertEquals(0, after)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observableChangesFlowEmitsCommittedEvents() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
import lyng.observable
|
||||||
|
|
||||||
|
val xs = [10,20].observable()
|
||||||
|
val it = xs.changes().iterator()
|
||||||
|
|
||||||
|
xs += 30
|
||||||
|
assert(it.hasNext())
|
||||||
|
val e1 = it.next()
|
||||||
|
assert(e1 is ListInsert<Int>)
|
||||||
|
assertEquals(2, (e1 as ListInsert<Int>).index)
|
||||||
|
assertEquals([30], e1.values)
|
||||||
|
|
||||||
|
xs[1] = 200
|
||||||
|
assert(it.hasNext())
|
||||||
|
val e2 = it.next()
|
||||||
|
assert(e2 is ListSet<Int>)
|
||||||
|
assertEquals(1, (e2 as ListSet<Int>).index)
|
||||||
|
assertEquals(20, e2.oldValue)
|
||||||
|
assertEquals(200, e2.newValue)
|
||||||
|
|
||||||
|
xs.removeAt(0)
|
||||||
|
assert(it.hasNext())
|
||||||
|
val e3 = it.next()
|
||||||
|
assert(e3 is ListRemove<Int>)
|
||||||
|
assertEquals(0, (e3 as ListRemove<Int>).index)
|
||||||
|
assertEquals(10, e3.oldValue)
|
||||||
|
|
||||||
|
it.cancelIteration()
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user