Skip to content

Commit

Permalink
Defer serializing event args to JSON
Browse files Browse the repository at this point in the history
Until we're on the Zipline thread instead of the UI thread.
  • Loading branch information
JakeWharton committed Jul 31, 2024
1 parent 0fbfeb6 commit c69ab16
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 71 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ New:
- Source-based schema parser is now the default. Can be disabled in your schema module with `redwood { useFir = false }`.

Changed:
- Nothing yet!
- In Treehouse, events from the UI are now serialized on a background thread. This means that there is both a delay and a thread change between when a UI binding sends an event and when that object is converted to JSON. All arguments to events must not be mutable and support property reads on any thread. Best practice is for all event arguments to be completely immutable.

Fixed:
- Nothing yet!
Expand Down
1 change: 1 addition & 0 deletions redwood-protocol-host/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ redwoodBuild {
}

apply plugin: 'com.github.gmazzo.buildconfig'
apply plugin: 'dev.drewhamilton.poko'

kotlin {
sourceSets {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import app.cash.redwood.protocol.ChildrenChange.Move
import app.cash.redwood.protocol.ChildrenChange.Remove
import app.cash.redwood.protocol.ChildrenTag
import app.cash.redwood.protocol.Create
import app.cash.redwood.protocol.EventSink
import app.cash.redwood.protocol.Id
import app.cash.redwood.protocol.ModifierChange
import app.cash.redwood.protocol.PropertyChange
Expand All @@ -50,7 +49,7 @@ public class HostProtocolAdapter<W : Any>(
guestVersion: RedwoodVersion,
container: Widget.Children<W>,
factory: ProtocolFactory<W>,
private val eventSink: EventSink,
private val eventSink: UiEventSink,
) : ChangesSink {
private val factory = requireNotNull(factory as? GeneratedProtocolFactory<W>) {
"Factory ${factory::class} was not generated by Redwood or is out of date"
Expand Down Expand Up @@ -378,7 +377,7 @@ private class RootProtocolNode<W : Any>(
Widget<W> {
private val children = ProtocolChildren(children)

override fun apply(change: PropertyChange, eventSink: EventSink) {
override fun apply(change: PropertyChange, eventSink: UiEventSink) {
throw AssertionError("unexpected: $change")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package app.cash.redwood.protocol.host
import app.cash.redwood.Modifier
import app.cash.redwood.RedwoodCodegenApi
import app.cash.redwood.protocol.ChildrenTag
import app.cash.redwood.protocol.EventSink
import app.cash.redwood.protocol.Id
import app.cash.redwood.protocol.PropertyChange
import app.cash.redwood.protocol.WidgetTag
Expand Down Expand Up @@ -48,7 +47,7 @@ public abstract class ProtocolNode<W : Any>(
/** Assigned when the node is added to the pool. */
internal var shapeHash = 0L

public abstract fun apply(change: PropertyChange, eventSink: EventSink)
public abstract fun apply(change: PropertyChange, eventSink: UiEventSink)

public fun updateModifier(modifier: Modifier) {
widget.modifier = modifier
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2024 Square, Inc.
*
* 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 app.cash.redwood.protocol.host

import app.cash.redwood.protocol.Event
import app.cash.redwood.protocol.EventSink
import app.cash.redwood.protocol.EventTag
import app.cash.redwood.protocol.Id
import dev.drewhamilton.poko.Poko
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.Json

/**
* A version of [Event] whose arguments have not yet been serialized to JSON and is thus
* cheap to create on the UI thread.
*/
@Poko
public class UiEvent(
public val id: Id,
public val tag: EventTag,
public val args: List<Any?> = emptyList(),
public val serializationStrategies: List<SerializationStrategy<Any?>> = emptyList(),
) {
init {
check(args.size == serializationStrategies.size) {
"Properties 'args' and 'serializationStrategies' must have the same size. " +
"Found ${args.size} and ${serializationStrategies.size}"
}
}

/** Serialize [args] into a JSON model using [serializationStrategies] into an [Event]. */
public fun toProtocol(json: Json): Event {
return Event(
id = id,
tag = tag,
args = List(args.size) {
json.encodeToJsonElement(serializationStrategies[it], args[it])
}
)
}
}

/** A version of [EventSink] which consumes [UiEvent]s. */
public fun interface UiEventSink {
public fun sendEvent(uiEvent: UiEvent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package app.cash.redwood.protocol.host
import app.cash.redwood.Modifier
import app.cash.redwood.RedwoodCodegenApi
import app.cash.redwood.protocol.ChildrenTag
import app.cash.redwood.protocol.EventSink
import app.cash.redwood.protocol.Id
import app.cash.redwood.protocol.PropertyChange
import app.cash.redwood.protocol.WidgetTag
Expand Down Expand Up @@ -128,7 +127,7 @@ class ChildrenNodeIndexTest {

@OptIn(RedwoodCodegenApi::class)
private class WidgetNode(override val widget: StringWidget) : ProtocolNode<String>(Id(1), WidgetTag(1)) {
override fun apply(change: PropertyChange, eventSink: EventSink) {
override fun apply(change: PropertyChange, eventSink: UiEventSink) {
throw UnsupportedOperationException()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import app.cash.redwood.layout.testing.RedwoodLayoutTestingWidgetFactory
import app.cash.redwood.lazylayout.testing.RedwoodLazyLayoutTestingWidgetFactory
import app.cash.redwood.protocol.ChildrenTag
import app.cash.redwood.protocol.Event
import app.cash.redwood.protocol.EventSink
import app.cash.redwood.protocol.EventTag
import app.cash.redwood.protocol.Id
import app.cash.redwood.protocol.ModifierElement
Expand Down Expand Up @@ -258,7 +257,7 @@ class ProtocolFactoryTest {
)
val textInput = factory.createNode(Id(1), WidgetTag(5))!!

val throwingEventSink = EventSink { error(it) }
val throwingEventSink = UiEventSink { error(it) }
textInput.apply(PropertyChange(Id(1), PropertyTag(2), JsonPrimitive("PT10S")), throwingEventSink)

assertThat((textInput.widget.value as TextInputValue).customType).isEqualTo(10.seconds)
Expand All @@ -275,7 +274,7 @@ class ProtocolFactoryTest {
val button = factory.createNode(Id(1), WidgetTag(4))!!

val change = PropertyChange(Id(1), PropertyTag(345432))
val eventSink = EventSink { throw UnsupportedOperationException() }
val eventSink = UiEventSink { throw UnsupportedOperationException() }
val t = assertFailsWith<IllegalArgumentException> {
button.apply(change, eventSink)
}
Expand Down Expand Up @@ -315,12 +314,12 @@ class ProtocolFactoryTest {
)
val textInput = factory.createNode(Id(1), WidgetTag(5))!!

val eventSink = RecordingEventSink()
val eventSink = RecordingUiEventSink()
textInput.apply(PropertyChange(Id(1), PropertyTag(4), JsonPrimitive(true)), eventSink)

(textInput.widget.value as TextInputValue).onChangeCustomType!!.invoke(10.seconds)

assertThat(eventSink.events.single())
assertThat(eventSink.events.single().toProtocol(json))
.isEqualTo(Event(Id(1), EventTag(4), listOf(JsonPrimitive("PT10S"))))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@
*/
package app.cash.redwood.protocol.host

import app.cash.redwood.protocol.Event
import app.cash.redwood.protocol.EventSink
class RecordingUiEventSink : UiEventSink {
val events = mutableListOf<UiEvent>()

class RecordingEventSink : EventSink {
val events = mutableListOf<Event>()

override fun sendEvent(event: Event) {
events += event
override fun sendEvent(uiEvent: UiEvent) {
events += uiEvent
}
}
Loading

0 comments on commit c69ab16

Please sign in to comment.