Skip to content

Commit

Permalink
Replace Text.onChanges() with Document event stream subscription (#111)
Browse files Browse the repository at this point in the history
* add TextOpInfo

* replace JsonText.onChanges() with events subscription
  • Loading branch information
7hong13 committed Jun 23, 2023
1 parent 2983e04 commit 315ce1d
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ class KanbanBoardViewModel(private val client: Client) : ViewModel() {
viewModelScope.launch {
client.events.collect {
if (it is Client.Event.DocumentSynced) {
try {
updateDocument(it.result.document.getRoot().getAs(DOCUMENT_LIST_KEY))
} catch (e: Exception) {
document.updateAsync { jsonObject ->
jsonObject.setNewArray(DOCUMENT_LIST_KEY)
}.await()
}
it.result.document.getRoot().getAsOrNull<JsonArray>(DOCUMENT_LIST_KEY)
?.let(::updateDocument)
?: run {
document.updateAsync { root ->
root.setNewArray(DOCUMENT_LIST_KEY)
}.await()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import dev.yorkie.core.Client
import dev.yorkie.core.Client.Event
import dev.yorkie.core.Client.PeersChangedResult.Unwatched
import dev.yorkie.document.Document
import dev.yorkie.document.crdt.TextChange
import dev.yorkie.document.json.JsonText
import dev.yorkie.document.operation.OperationInfo
import dev.yorkie.document.time.ActorID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -29,8 +29,8 @@ class EditorViewModel(private val client: Client) : ViewModel(), YorkieEditText.
private val _content = MutableSharedFlow<String>()
val content = _content.asSharedFlow()

private val _textChanges = MutableSharedFlow<TextChange>()
val textChanges = _textChanges.asSharedFlow()
private val _textChangeInfos = MutableSharedFlow<Pair<ActorID, OperationInfo.TextOpInfo>>()
val textChangeInfos = _textChangeInfos.asSharedFlow()

val removedPeers = client.events.filterIsInstance<Event.PeersChanged>()
.map { it.result }
Expand All @@ -42,38 +42,39 @@ class EditorViewModel(private val client: Client) : ViewModel(), YorkieEditText.
val peerSelectionInfos: Map<ActorID, PeerSelectionInfo>
get() = _peerSelectionInfos

private val remoteChangeEventHandler: ((List<TextChange>) -> Unit) = { changes ->
val clientID = client.requireClientId()
changes.filterNot { it.actor == clientID }.forEach {
viewModelScope.launch {
_textChanges.emit(it)
}
}
}

init {
viewModelScope.launch {
if (client.activateAsync().await() && client.attachAsync(document).await()) {
document.getRoot().getAsOrNull<JsonText>(TEXT_KEY)
?.onChanges(remoteChangeEventHandler)
?: run {
document.updateAsync {
it.setNewText(TEXT_KEY).onChanges(remoteChangeEventHandler)
}.await()
}
if (document.getRoot().getAsOrNull<JsonText>(TEXT_KEY) == null) {
document.updateAsync {
it.setNewText(TEXT_KEY)
}.await()
}
client.syncAsync().await()
}
}

viewModelScope.launch {
document.events.collect {
if (it is Document.Event.Snapshot) {
document.events.collect { event ->
if (event is Document.Event.Snapshot) {
syncText()
} else if (event is Document.Event.RemoteChange) {
emitTextChanges(event.changeInfos)
}
}
}
}

private suspend fun emitTextChanges(changeInfos: List<Document.Event.ChangeInfo>) {
val clientID = client.requireClientId()
changeInfos.filterNot { it.actorID == clientID }
.flatMap { (_, ops, actor) ->
ops.filterIsInstance<OperationInfo.TextOpInfo>().map { op -> actor to op }
}.forEach {
_textChangeInfos.emit(it)
}
}

fun syncText() {
viewModelScope.launch {
val content = document.getRoot().getAsOrNull<JsonText>(TEXT_KEY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.texteditor.databinding.ActivityMainBinding
import dev.yorkie.core.Client
import dev.yorkie.document.crdt.TextChange
import dev.yorkie.document.crdt.TextChangeType
import dev.yorkie.document.operation.OperationInfo
import dev.yorkie.document.time.ActorID
import dev.yorkie.util.Logger
import dev.yorkie.util.YorkieLogger
Expand Down Expand Up @@ -52,11 +51,10 @@ class MainActivity : AppCompatActivity() {
}

launch {
viewModel.textChanges.collect {
when (it.type) {
TextChangeType.Content -> it.handleContentChange()
TextChangeType.Selection -> it.handleSelectChange()
else -> return@collect
viewModel.textChangeInfos.collect { (actor, opInfo) ->
when (opInfo) {
is OperationInfo.EditOpInfo -> opInfo.handleContentChange()
is OperationInfo.SelectOpInfo -> opInfo.handleSelectChange(actor)
}
}
}
Expand All @@ -72,21 +70,21 @@ class MainActivity : AppCompatActivity() {
}
}

private fun TextChange.handleContentChange() {
private fun OperationInfo.EditOpInfo.handleContentChange() {
binding.textEditor.withRemoteChange {
if (from == to) {
it.text.insert(from.coerceAtLeast(0), content.orEmpty())
it.text.insert(from.coerceAtLeast(0), value.text)
} else {
it.text.replace(
from.coerceAtLeast(0),
to.coerceAtLeast(0),
content.orEmpty(),
value.text,
)
}
}
}

private fun TextChange.handleSelectChange() {
private fun OperationInfo.SelectOpInfo.handleSelectChange(actor: ActorID) {
val editable = binding.textEditor.text ?: return

if (editable.removePrevSpan(actor) && from == to) {
Expand Down
33 changes: 2 additions & 31 deletions yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtText.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ internal data class CrdtText(
) : CrdtElement() {
private val selectionMap = mutableMapOf<ActorID, Selection>()

private var onChangesHandler: ((List<TextChange>) -> Unit)? = null

@Volatile
private var remoteChangeLock: Boolean = false

val removedNodesLength: Int
get() = rgaTreeSplit.removedNodesLength

Expand Down Expand Up @@ -71,7 +66,6 @@ internal data class CrdtText(
changes[changes.lastIndex] = changes.last().copy(attributes = attributes)
}
selectPrev(RgaTreeSplitNodeRange(caretPos, caretPos), executedAt)?.let { changes.add(it) }
handleChanges(changes)
return latestCreatedAtMap to changes
}

Expand Down Expand Up @@ -101,7 +95,7 @@ internal data class CrdtText(
val fromRight = rgaTreeSplit.findNodeWithSplit(range.first, executedAt).second

// 2. Style nodes between from and to.
val changes = rgaTreeSplit.findBetween(fromRight, toRight)
return rgaTreeSplit.findBetween(fromRight, toRight)
.filterNot { it.isRemoved }
.map { node ->
val (fromIndex, toIndex) = rgaTreeSplit.findIndexesFromRange(node.createRange())
Expand All @@ -115,22 +109,13 @@ internal data class CrdtText(
attributes,
)
}

handleChanges(changes)
return changes
}

/**
* Stores that the given [range] has been selected.
*/
fun select(range: RgaTreeSplitNodeRange, executedAt: TimeTicket): TextChange? {
return if (remoteChangeLock) {
null
} else {
selectPrev(range, executedAt)?.also {
handleChanges(listOf(it))
}
}
return selectPrev(range, executedAt)
}

/**
Expand All @@ -153,21 +138,7 @@ internal data class CrdtText(
return copy(rgaTreeSplit = rgaTreeSplit.deepCopy())
}

/**
* Registers a handler of onChanges event.
*/
fun onChanges(handler: ((List<TextChange>) -> Unit)) {
onChangesHandler = handler
}

override fun toString(): String {
return rgaTreeSplit.toString()
}

private fun handleChanges(changes: List<TextChange>) {
onChangesHandler ?: return
remoteChangeLock = true
onChangesHandler?.invoke(changes)
remoteChangeLock = false
}
}
8 changes: 2 additions & 6 deletions yorkie/src/main/kotlin/dev/yorkie/document/crdt/TextInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import dev.yorkie.document.json.escapeString
import dev.yorkie.document.time.ActorID
import dev.yorkie.document.time.TimeTicket

/**
* The value passed as an argument to [CrdtText.onChanges].
* [CrdtText.onChanges] is called when the [CrdtText] is modified.
*/
public data class TextChange(
internal data class TextChange(
val type: TextChangeType,
val actor: ActorID,
val from: Int,
Expand All @@ -20,7 +16,7 @@ public data class TextChange(
/**
* The type of [TextChange].
*/
public enum class TextChangeType {
internal enum class TextChangeType {
Content, Selection, Style
}

Expand Down
90 changes: 75 additions & 15 deletions yorkie/src/main/kotlin/dev/yorkie/document/json/JsonText.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package dev.yorkie.document.json

import dev.yorkie.document.change.ChangeContext
import dev.yorkie.document.crdt.CrdtText
import dev.yorkie.document.crdt.TextChange
import dev.yorkie.document.crdt.RgaTreeSplitNodeRange
import dev.yorkie.document.crdt.TextWithAttributes
import dev.yorkie.document.operation.EditOperation
import dev.yorkie.document.operation.SelectOperation
Expand Down Expand Up @@ -37,9 +37,27 @@ public class JsonText internal constructor(
return false
}

val range = target.createRange(fromIndex, toIndex)
val range = createRange(fromIndex, toIndex) ?: return false

YorkieLogger.d(
TAG,
"EDIT: f:$fromIndex->${range.first}, t:$toIndex->${range.second} c:$content",
)

val executedAt = context.issueTimeTicket()
val maxCreatedAtMapByActor = target.edit(range, content, executedAt, attributes).first
val maxCreatedAtMapByActor = runCatching {
target.edit(range, content, executedAt, attributes).first
}.getOrElse {
when (it) {
is NoSuchElementException, is IllegalArgumentException -> {
YorkieLogger.e(TAG, "can't style text")
return false
}

else -> throw it
}
}

context.push(
EditOperation(
fromPos = range.first,
Expand All @@ -48,7 +66,7 @@ public class JsonText internal constructor(
content = content,
parentCreatedAt = target.createdAt,
executedAt = executedAt,
attributes = attributes ?: mapOf(),
attributes = attributes ?: emptyMap(),
),
)

Expand All @@ -71,9 +89,26 @@ public class JsonText internal constructor(
return false
}

val range = target.createRange(fromIndex, toIndex)
val range = createRange(fromIndex, toIndex) ?: return false

YorkieLogger.d(
TAG,
"STYL: f:$fromIndex->${range.first}, t:$toIndex->${range.second} a:$attributes",
)

val executedAt = context.issueTimeTicket()
target.style(range, attributes, executedAt)
runCatching {
target.style(range, attributes, executedAt)
}.getOrElse {
when (it) {
is NoSuchElementException, is IllegalArgumentException -> {
YorkieLogger.e(TAG, "can't style text")
return false
}

else -> throw it
}
}

context.push(
StyleOperation(
Expand All @@ -91,9 +126,26 @@ public class JsonText internal constructor(
* Selects the given range.
*/
public fun select(fromIndex: Int, toIndex: Int): Boolean {
val range = target.createRange(fromIndex, toIndex)
if (fromIndex > toIndex) {
YorkieLogger.e(TAG, "fromIndex should be less than or equal to toIndex")
return false
}

val range = createRange(fromIndex, toIndex) ?: return false

YorkieLogger.d(
TAG,
"SELT: f:$fromIndex->${range.first}, t:$toIndex->${range.second}",
)

val executedAt = context.issueTimeTicket()
target.select(range, executedAt)
try {
target.select(range, executedAt)
} catch (e: NoSuchElementException) {
YorkieLogger.e(TAG, "can't select text")
return false
}

context.push(
SelectOperation(
parentCreatedAt = target.createdAt,
Expand All @@ -105,6 +157,21 @@ public class JsonText internal constructor(
return true
}

private fun createRange(fromIndex: Int, toIndex: Int): RgaTreeSplitNodeRange? {
return runCatching {
target.createRange(fromIndex, toIndex)
}.getOrElse {
when (it) {
is NoSuchElementException, is IndexOutOfBoundsException -> {
YorkieLogger.e(TAG, "can't create range")
null
}

else -> throw it
}
}
}

/**
* Deletes the text in the given range.
*/
Expand All @@ -119,13 +186,6 @@ public class JsonText internal constructor(
return edit(0, target.length, "")
}

/**
* Registers a handler of onChanges event.
*/
public fun onChanges(handler: ((List<TextChange>) -> Unit)) {
return target.onChanges(handler)
}

override fun toString(): String {
return target.toString()
}
Expand Down
Loading

0 comments on commit 315ce1d

Please sign in to comment.