Repository: cashapp/redwood
Pull request #598 (2022-12-06 18:15:16)

Add JVM to Treehouse host source set
Previously it was always there but was neither host nor JS so it only had a weird subset of common code. Note that it still lacks a launcher so it is not yet usable.


Patch:
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b36663ecf..fe33c471a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,7 +8,7 @@ androidx-compose = "1.2.1"
 jbComposeCompiler = "1.3.2.1"
 jbComposeRuntime = "1.2.1"
 lint = "30.4.0-alpha09"
-zipline = "0.9.10"
+zipline = "0.9.12"
 coil = "2.2.2"
 
 [libraries]
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
index 7345461e2..f5607f8f4 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
@@ -34,7 +34,6 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeName
 import com.squareup.kotlinpoet.TypeSpec
-import com.squareup.kotlinpoet.U_INT
 import com.squareup.kotlinpoet.asTypeName
 
 /*
@@ -337,13 +336,13 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
           addFunction(
             FunSpec.builder("children")
               .addModifiers(OVERRIDE)
-              .addParameter("tag", U_INT)
+              .addParameter("tag", Protocol.ChildrenTag)
               .returns(RedwoodWidget.WidgetChildrenOfW.copy(nullable = true))
               .apply {
                 if (childrens.isNotEmpty()) {
-                  beginControlFlow("return when (tag)")
+                  beginControlFlow("return when (tag.value)")
                   for (children in childrens) {
-                    addStatement("%LU -> widget.%N", children.tag, children.name)
+                    addStatement("%L -> widget.%N", children.tag, children.name)
                   }
                   beginControlFlow("else ->")
                   addStatement("mismatchHandler.onUnknownChildren(%L, tag)", widget.tag)
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt
index 156e24d42..45c6ad859 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt
@@ -264,7 +264,11 @@ internal fun generateDiffProducingWidget(schema: Schema, widget: Widget, host: S
                     .addModifiers(OVERRIDE)
                     .getter(
                       FunSpec.getterBuilder()
-                        .addStatement("return bridge.widgetChildren(id, %LU)", trait.tag)
+                        .addStatement(
+                          "return bridge.widgetChildren(id, %T(%L))",
+                          Protocol.ChildrenTag,
+                          trait.tag,
+                        )
                         .build(),
                     )
                     .build(),
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt
index a737fb440..9e72b33d6 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt
@@ -26,6 +26,7 @@ import com.squareup.kotlinpoet.TypeVariableName
 import com.squareup.kotlinpoet.UNIT
 
 internal object Protocol {
+  val ChildrenTag = ClassName("app.cash.redwood.protocol", "ChildrenTag")
   val Event = ClassName("app.cash.redwood.protocol", "Event")
   val EventTag = ClassName("app.cash.redwood.protocol", "EventTag")
   val EventSink = ClassName("app.cash.redwood.protocol", "EventSink")
diff --git a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetChildren.kt b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetChildren.kt
index 41454c6c9..9f91cbb25 100644
--- a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetChildren.kt
+++ b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetChildren.kt
@@ -16,12 +16,13 @@
 package app.cash.redwood.protocol.compose
 
 import app.cash.redwood.protocol.ChildrenDiff
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.Id
 import app.cash.redwood.widget.Widget
 
 internal class DiffProducingWidgetChildren(
   private val id: Id,
-  private val tag: UInt,
+  private val tag: ChildrenTag,
   private val bridge: ProtocolBridge,
 ) : Widget.Children<Nothing> {
   private val ids = mutableListOf<Id>()
diff --git a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolBridge.kt b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolBridge.kt
index 2681439d8..37b1a1ad6 100644
--- a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolBridge.kt
+++ b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolBridge.kt
@@ -16,6 +16,7 @@
 package app.cash.redwood.protocol.compose
 
 import app.cash.redwood.protocol.ChildrenDiff
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.Diff
 import app.cash.redwood.protocol.Event
 import app.cash.redwood.protocol.EventSink
@@ -83,7 +84,7 @@ public class ProtocolBridge : EventSink {
     nodes.remove(id)
   }
 
-  public fun widgetChildren(id: Id, tag: UInt): Widget.Children<Nothing> {
+  public fun widgetChildren(id: Id, tag: ChildrenTag): Widget.Children<Nothing> {
     return DiffProducingWidgetChildren(id, tag, this)
   }
 
diff --git a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolRedwoodComposition.kt b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolRedwoodComposition.kt
index ddc929901..12eaa3340 100644
--- a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolRedwoodComposition.kt
+++ b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolRedwoodComposition.kt
@@ -22,7 +22,7 @@ import androidx.compose.runtime.currentComposer
 import app.cash.redwood.compose.LocalWidgetVersion
 import app.cash.redwood.compose.RedwoodComposition
 import app.cash.redwood.compose.WidgetApplier
-import app.cash.redwood.protocol.ChildrenDiff
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.DiffSink
 import app.cash.redwood.protocol.Id
 import kotlinx.coroutines.CoroutineScope
@@ -38,7 +38,7 @@ public fun ProtocolRedwoodComposition(
   widgetVersion: UInt,
 ): RedwoodComposition {
   val bridge = factory.bridge
-  val root = DiffProducingWidgetChildren(Id.Root, ChildrenDiff.RootChildrenTag, bridge)
+  val root = DiffProducingWidgetChildren(Id.Root, ChildrenTag.Root, bridge)
   val applier = WidgetApplier(factory, root) {
     bridge.createDiffOrNull()?.let(diffSink::sendDiff)
   }
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt
index f66389e56..48c8f0e82 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt
@@ -15,6 +15,7 @@
  */
 package app.cash.redwood.protocol.widget
 
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.EventSink
 import app.cash.redwood.protocol.Id
 import app.cash.redwood.protocol.PropertyDiff
@@ -47,7 +48,7 @@ public abstract class DiffConsumingNode<W : Any>(
    * If `null` is returned, the caller should make every effort to ignore these children and
    * continue executing.
    */
-  public abstract fun children(tag: UInt): Widget.Children<W>?
+  public abstract fun children(tag: ChildrenTag): Widget.Children<W>?
 
   public interface Factory<W : Any> {
     /**
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt
index 810d7eb6c..b4ca388a8 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt
@@ -16,7 +16,7 @@
 package app.cash.redwood.protocol.widget
 
 import app.cash.redwood.protocol.ChildrenDiff
-import app.cash.redwood.protocol.ChildrenDiff.Companion.RootChildrenTag
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.Diff
 import app.cash.redwood.protocol.DiffSink
 import app.cash.redwood.protocol.EventSink
@@ -103,8 +103,8 @@ private class DiffConsumingProtocolRoot<W : Any>(
     throw AssertionError("unexpected: $diff")
   }
 
-  override fun children(tag: UInt) = when (tag) {
-    RootChildrenTag -> children
+  override fun children(tag: ChildrenTag) = when (tag) {
+    ChildrenTag.Root -> children
     else -> throw AssertionError("unexpected: $tag")
   }
 
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt
index 680413870..28a98e948 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt
@@ -15,6 +15,7 @@
  */
 package app.cash.redwood.protocol.widget
 
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.PropertyTag
 import kotlin.jvm.JvmField
 
@@ -31,7 +32,7 @@ public interface ProtocolMismatchHandler {
   public fun onUnknownLayoutModifier(tag: Int)
 
   /** Handle a request to manipulate unknown children [tag] for the specified widget [kind]. */
-  public fun onUnknownChildren(kind: Int, tag: UInt)
+  public fun onUnknownChildren(kind: Int, tag: ChildrenTag)
 
   /** Handle a request to set an unknown property [tag] for the specified widget [kind]. */
   public fun onUnknownProperty(kind: Int, tag: PropertyTag)
@@ -48,8 +49,8 @@ public interface ProtocolMismatchHandler {
         throw IllegalArgumentException("Unknown layout modifier tag $tag")
       }
 
-      override fun onUnknownChildren(kind: Int, tag: UInt) {
-        throw IllegalArgumentException("Unknown children tag $tag for widget kind $kind")
+      override fun onUnknownChildren(kind: Int, tag: ChildrenTag) {
+        throw IllegalArgumentException("Unknown children tag ${tag.value} for widget kind $kind")
       }
 
       override fun onUnknownProperty(kind: Int, tag: PropertyTag) {
diff --git a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt
index 3545e184e..3506d5485 100644
--- a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt
+++ b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt
@@ -63,13 +63,13 @@ public sealed class ChildrenDiff {
   public abstract val id: Id
 
   /** Identifies which group of children changed on the widget with [id]. */
-  public abstract val tag: UInt
+  public abstract val tag: ChildrenTag
 
   @Serializable
   @SerialName("insert")
   public data class Insert(
     override val id: Id,
-    override val tag: UInt,
+    override val tag: ChildrenTag,
     val childId: Id,
     val kind: Int,
     val index: Int,
@@ -79,7 +79,7 @@ public sealed class ChildrenDiff {
   @SerialName("move")
   public data class Move(
     override val id: Id,
-    override val tag: UInt,
+    override val tag: ChildrenTag,
     val fromIndex: Int,
     val toIndex: Int,
     val count: Int,
@@ -89,12 +89,8 @@ public sealed class ChildrenDiff {
   @SerialName("remove")
   public data class Remove(
     override val id: Id,
-    override val tag: UInt,
+    override val tag: ChildrenTag,
     val index: Int,
     val count: Int,
   ) : ChildrenDiff()
-
-  public companion object {
-    public const val RootChildrenTag: UInt = 1U
-  }
 }
diff --git a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/values.kt b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/values.kt
index ed63b7687..302b08859 100644
--- a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/values.kt
+++ b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/values.kt
@@ -34,3 +34,11 @@ public value class EventTag(public val value: Int)
 @JvmInline
 @Serializable
 public value class PropertyTag(public val value: Int)
+
+@JvmInline
+@Serializable
+public value class ChildrenTag(public val value: Int) {
+  public companion object {
+    public val Root: ChildrenTag = ChildrenTag(0)
+  }
+}
diff --git a/redwood-treehouse/build.gradle b/redwood-treehouse/build.gradle
index bbef3781a..93fba5d67 100644
--- a/redwood-treehouse/build.gradle
+++ b/redwood-treehouse/build.gradle
@@ -42,12 +42,18 @@ kotlin {
     nativeMain {
       dependsOn(hostMain)
     }
-    androidMain {
+    commonJvmMain {
       dependsOn(hostMain)
       dependencies {
         api libs.okHttp
       }
     }
+    androidMain {
+      dependsOn(commonJvmMain)
+    }
+    jvmMain {
+      dependsOn(commonJvmMain)
+    }
 
     hostTest {
       dependencies {
@@ -59,12 +65,18 @@ kotlin {
     nativeTest {
       dependsOn(hostTest)
     }
-    androidTest {
+    commonJvmTest {
       dependsOn(hostTest)
+    }
+    androidTest {
+      dependsOn(commonJvmTest)
       dependencies {
         implementation libs.robolectric
       }
     }
+    jvmTest {
+      dependsOn(commonJvmTest)
+    }
   }
 }
 
diff --git a/redwood-treehouse/src/androidMain/kotlin/app/cash/redwood/treehouse/hotReloadAndroid.kt b/redwood-treehouse/src/commonJvmMain/kotlin/app/cash/redwood/treehouse/hotReloadOkHttp.kt
similarity index 100%
rename from redwood-treehouse/src/androidMain/kotlin/app/cash/redwood/treehouse/hotReloadAndroid.kt
rename to redwood-treehouse/src/commonJvmMain/kotlin/app/cash/redwood/treehouse/hotReloadOkHttp.kt
diff --git a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventListener.kt b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventListener.kt
index 636f9713e..2915f9c4e 100644
--- a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventListener.kt
+++ b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventListener.kt
@@ -15,34 +15,66 @@
  */
 package app.cash.redwood.treehouse
 
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.PropertyTag
 import app.cash.zipline.Call
 import app.cash.zipline.CallResult
+import app.cash.zipline.Zipline
 import app.cash.zipline.ZiplineService
 
 public abstract class EventListener {
+  /**
+   * Invoked each time a [TreehouseApp] is created. When this is triggered the app may not yet have
+   * any code loaded; but it will always attempt to load code.
+   */
   public open fun appCreated(
     app: TreehouseApp<*>,
   ) {
   }
 
+  /**
+   * Invoked with [TreehouseApp.cancel] when the application is shut down.
+   *
+   * This is different from [codeUnloaded] which occurs during hot reloads; this only occurs if the
+   * app itself is explicitly closed.
+   */
   public open fun appCanceled(
     app: TreehouseApp<*>,
   ) {
   }
 
+  /**
+   * Invoked for each attempt at loading code. This will be followed by a [codeLoadSuccess],
+   * [codeLoadFailed], or [codeLoadSkipped] if the code is unchanged.
+   *
+   * @return any object. This value will be passed back to one of the above functions. The base
+   *     function always returns null.
+   */
   public open fun codeLoadStart(
     app: TreehouseApp<*>,
     manifestUrl: String?,
   ): Any? = null
 
+  /**
+   * Invoked when code is successfully downloaded and initialized.
+   *
+   * @param startValue the value returned by [codeLoadStart] for the start of this call. This
+   *   is null unless [codeLoadStart] is overridden to return something else.
+   */
   public open fun codeLoadSuccess(
     app: TreehouseApp<*>,
     manifestUrl: String?,
+    zipline: Zipline,
     startValue: Any?,
   ) {
   }
 
+  /**
+   * Invoked when a code load is skipped because the code hasn't changed.
+   *
+   * @param startValue the value returned by [codeLoadStart] for the start of this call. This
+   *   is null unless [codeLoadStart] is overridden to return something else.
+   */
   public open fun codeLoadSkipped(
     app: TreehouseApp<*>,
     manifestUrl: String?,
@@ -50,6 +82,12 @@ public abstract class EventListener {
   ) {
   }
 
+  /**
+   * Invoked when a code load fails.
+   *
+   * @param startValue the value returned by [codeLoadStart] for the start of this call. This
+   *   is null unless [codeLoadStart] is overridden to return something else.
+   */
   public open fun codeLoadFailed(
     app: TreehouseApp<*>,
     manifestUrl: String?,
@@ -58,25 +96,47 @@ public abstract class EventListener {
   ) {
   }
 
+  /**
+   * Invoked when code is unloaded because it is no longer needed. Typically this occurs when a hot
+   * code update is applied.
+   */
+  public open fun codeUnloaded(
+    app: TreehouseApp<*>,
+    zipline: Zipline,
+  ) {
+  }
+
+  /**
+   * Invoked on a request to create an unknown widget [kind].
+   */
   public open fun onUnknownWidget(
     app: TreehouseApp<*>,
     kind: Int,
   ) {
   }
 
+  /**
+   * Invoked on a request to create an unknown layout modifier [tag].
+   */
   public open fun onUnknownLayoutModifier(
     app: TreehouseApp<*>,
     tag: Int,
   ) {
   }
 
+  /**
+   * Invoked on a request to manipulate unknown children [tag] for the specified widget [kind].
+   */
   public open fun onUnknownChildren(
     app: TreehouseApp<*>,
     kind: Int,
-    tag: UInt,
+    tag: ChildrenTag,
   ) {
   }
 
+  /**
+   * Invoked on a request to set an unknown property [tag] for the specified widget [kind].
+   */
   public open fun onUnknownProperty(
     app: TreehouseApp<*>,
     kind: Int,
@@ -84,11 +144,24 @@ public abstract class EventListener {
   ) {
   }
 
+  /**
+   * Invoked when a network download starts. This will be followed by [downloadSuccess] or
+   * [downloadFailed].
+   *
+   * @return any object. This value will be passed back to one of the above functions. The base
+   *     function always returns null.
+   */
   public open fun downloadStart(
     app: TreehouseApp<*>,
     url: String,
   ): Any? = null
 
+  /**
+   * Invoked when a network download completes successfully.
+   *
+   * @param startValue the value returned by [downloadStart] for the start of this call. This
+   *   is null unless [downloadStart] is overridden to return something else.
+   */
   public open fun downloadSuccess(
     app: TreehouseApp<*>,
     url: String,
@@ -96,6 +169,12 @@ public abstract class EventListener {
   ) {
   }
 
+  /**
+   * Invoked when a network download fails.
+   *
+   * @param startValue the value returned by [downloadStart] for the start of this call. This
+   *   is null unless [downloadStart] is overridden to return something else.
+   */
   public open fun downloadFailed(
     app: TreehouseApp<*>,
     url: String,
@@ -104,6 +183,10 @@ public abstract class EventListener {
   ) {
   }
 
+  /**
+   * Invoked when the manifest couldn't be decoded as JSON. For example, this might occur if there's
+   * a captive portal on the network.
+   */
   public open fun manifestParseFailed(
     app: TreehouseApp<*>,
     url: String?,
@@ -111,30 +194,59 @@ public abstract class EventListener {
   ) {
   }
 
+  /**
+   * Invoked when something calls [Zipline.bind], or a service is sent via an API.
+   */
   public open fun bindService(
+    app: TreehouseApp<*>,
     name: String,
     service: ZiplineService,
   ) {
   }
 
+  /**
+   * Invoked when something calls [Zipline.take], or a service is received via an API.
+   */
   public open fun takeService(
+    app: TreehouseApp<*>,
     name: String,
     service: ZiplineService,
   ) {
   }
 
+  /**
+   * Invoked when a service function is called. This may be invoked for either suspending or
+   * non-suspending functions.
+   *
+   * @return any object. This value will be passed back to [callEnd] when the call is completed. The
+   *   base function always returns null.
+   */
   public open fun callStart(
+    app: TreehouseApp<*>,
     call: Call,
   ): Any? = null
 
+  /**
+   * Invoked when a service function call completes.
+   *
+   * @param startValue the value returned by [callStart] for the start of this call. This is null
+   *   unless [callStart] is overridden to return something else.
+   */
   public open fun callEnd(
+    app: TreehouseApp<*>,
     call: Call,
     result: CallResult,
     startValue: Any?,
   ) {
   }
 
+  /**
+   * Invoked when a service is garbage collected without being closed.
+   *
+   * Note that this method may be invoked after [codeUnloaded].
+   */
   public open fun serviceLeaked(
+    app: TreehouseApp<*>,
     name: String,
   ) {
   }
diff --git a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt
index 905fd06a7..7245e16af 100644
--- a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt
+++ b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt
@@ -15,6 +15,7 @@
  */
 package app.cash.redwood.treehouse
 
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.PropertyTag
 import app.cash.redwood.protocol.widget.ProtocolMismatchHandler
 import app.cash.zipline.Call
@@ -38,9 +39,8 @@ internal class EventPublisher(
     listener.appCanceled(app)
   }
 
-  val ziplineEventListener = object : ZiplineEventListener() {
+  fun ziplineEventListener(app: TreehouseApp<*>) = object : ZiplineEventListener() {
     override fun applicationLoadStart(applicationName: String, manifestUrl: String?): Any? {
-      val app = nameToApplication[applicationName]!!
       return listener.codeLoadStart(app, manifestUrl)
     }
 
@@ -50,8 +50,7 @@ internal class EventPublisher(
       zipline: Zipline,
       startValue: Any?,
     ) {
-      val app = nameToApplication[applicationName]!!
-      listener.codeLoadSuccess(app, manifestUrl, startValue)
+      listener.codeLoadSuccess(app, manifestUrl, zipline, startValue)
     }
 
     override fun applicationLoadSkipped(
@@ -59,7 +58,6 @@ internal class EventPublisher(
       manifestUrl: String,
       startValue: Any?,
     ) {
-      val app = nameToApplication[applicationName]!!
       listener.codeLoadSkipped(app, manifestUrl, startValue)
     }
 
@@ -69,29 +67,26 @@ internal class EventPublisher(
       exception: Exception,
       startValue: Any?,
     ) {
-      val app = nameToApplication[applicationName]!!
       listener.codeLoadFailed(app, manifestUrl, exception, startValue)
     }
 
     override fun bindService(zipline: Zipline, name: String, service: ZiplineService) {
-      listener.bindService(name, service)
+      listener.bindService(app, name, service)
     }
 
     override fun callStart(zipline: Zipline, call: Call): Any? {
-      return listener.callStart(call)
+      return listener.callStart(app, call)
     }
 
     override fun callEnd(zipline: Zipline, call: Call, result: CallResult, startValue: Any?) {
-      listener.callEnd(call, result, startValue)
+      listener.callEnd(app, call, result, startValue)
     }
 
     override fun downloadStart(applicationName: String, url: String): Any? {
-      val app = nameToApplication[applicationName]!!
       return listener.downloadStart(app, url)
     }
 
     override fun downloadEnd(applicationName: String, url: String, startValue: Any?) {
-      val app = nameToApplication[applicationName]!!
       listener.downloadSuccess(app, url, startValue)
     }
 
@@ -101,21 +96,23 @@ internal class EventPublisher(
       exception: Exception,
       startValue: Any?,
     ) {
-      val app = nameToApplication[applicationName]!!
       listener.downloadFailed(app, url, exception, startValue)
     }
 
     override fun manifestParseFailed(applicationName: String, url: String?, exception: Exception) {
-      val app = nameToApplication[applicationName]!!
       listener.manifestParseFailed(app, url, exception)
     }
 
     override fun takeService(zipline: Zipline, name: String, service: ZiplineService) {
-      listener.takeService(name, service)
+      listener.takeService(app, name, service)
     }
 
     override fun serviceLeaked(zipline: Zipline, name: String) {
-      listener.serviceLeaked(name)
+      listener.serviceLeaked(app, name)
+    }
+
+    override fun ziplineClosed(zipline: Zipline) {
+      listener.codeUnloaded(app, zipline)
     }
   }
 
@@ -128,7 +125,7 @@ internal class EventPublisher(
       listener.onUnknownLayoutModifier(app, tag)
     }
 
-    override fun onUnknownChildren(kind: Int, tag: UInt) {
+    override fun onUnknownChildren(kind: Int, tag: ChildrenTag) {
       listener.onUnknownChildren(app, kind, tag)
     }
 
diff --git a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/FrameClockServiceHost.kt b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/FrameClockService.kt
similarity index 100%
rename from redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/FrameClockServiceHost.kt
rename to redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/FrameClockService.kt
diff --git a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseLauncher.kt b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseLauncher.kt
index 5dc15ab33..ec3a86b6f 100644
--- a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseLauncher.kt
+++ b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseLauncher.kt
@@ -37,8 +37,8 @@ public class TreehouseLauncher internal constructor(
   private val platform: TreehousePlatform,
   public val dispatchers: TreehouseDispatchers,
   eventListener: EventListener,
-  httpClient: ZiplineHttpClient,
-  manifestVerifier: ManifestVerifier,
+  private val httpClient: ZiplineHttpClient,
+  private val manifestVerifier: ManifestVerifier,
   private val embeddedDir: Path,
   private val embeddedFileSystem: FileSystem,
   private val cacheName: String,
@@ -46,29 +46,11 @@ public class TreehouseLauncher internal constructor(
 ) : Closeable {
   private val eventPublisher = EventPublisher(eventListener)
 
-  /** Loads applications from the network only. The cache is neither read nor written. */
-  private val ziplineLoaderNetworkOnly = ZiplineLoader(
-    dispatcher = dispatchers.zipline,
-    manifestVerifier = manifestVerifier,
-    httpClient = httpClient,
-    eventListener = eventPublisher.ziplineEventListener,
-  )
-
   /** This is lazy to avoid initializing the cache on the thread that creates this launcher. */
   private val cache: ZiplineCache by lazy {
     platform.newCache(name = cacheName, maxSizeInBytes = cacheMaxSizeInBytes)
   }
 
-  /** Loads applications from the network, the cache, or the embedded resources. */
-  private val ziplineLoaderNetworkCacheEmbedded: ZiplineLoader by lazy {
-    ziplineLoaderNetworkOnly.withCache(
-      cache = cache,
-    ).withEmbedded(
-      embeddedDir = embeddedDir,
-      embeddedFileSystem = embeddedFileSystem,
-    )
-  }
-
   public fun <A : AppService> launch(
     scope: CoroutineScope,
     spec: TreehouseApp.Spec<A>,
@@ -83,7 +65,7 @@ public class TreehouseLauncher internal constructor(
     eventPublisher.appCreated(treehouseApp)
 
     scope.launch(dispatchers.zipline) {
-      val ziplineFileFlow = ziplineFlow(spec)
+      val ziplineFileFlow = ziplineFlow(treehouseApp)
       ziplineFileFlow.collect {
         when (it) {
           is LoadResult.Success -> {
@@ -91,7 +73,7 @@ public class TreehouseLauncher internal constructor(
             treehouseApp.onCodeChanged(it.zipline, app)
           }
           is LoadResult.Failure -> {
-            // TODO
+            // EventListener already notified.
           }
         }
       }
@@ -104,10 +86,30 @@ public class TreehouseLauncher internal constructor(
    * Continuously polls for updated code, and emits a new [LoadResult] instance when new code is
    * found.
    */
-  private fun ziplineFlow(spec: TreehouseApp.Spec<*>): Flow<LoadResult> {
+  private fun ziplineFlow(treehouseApp: TreehouseApp<*>): Flow<LoadResult> {
+    val spec = treehouseApp.spec
+
+    // Loads applications from the network only. The cache is neither read nor written.
+    val ziplineLoaderNetworkOnly = ZiplineLoader(
+      dispatcher = dispatchers.zipline,
+      manifestVerifier = manifestVerifier,
+      httpClient = httpClient,
+      eventListener = eventPublisher.ziplineEventListener(treehouseApp),
+    )
+
     val ziplineLoaderForLoad = when (spec.freshCodePolicy) {
-      FreshCodePolicy.ALWAYS_REFRESH_IMMEDIATELY -> ziplineLoaderNetworkOnly
-      else -> ziplineLoaderNetworkCacheEmbedded
+      FreshCodePolicy.ALWAYS_REFRESH_IMMEDIATELY -> {
+        ziplineLoaderNetworkOnly
+      }
+      else -> {
+        // Loads applications from the network, the cache, or the embedded resources.
+        ziplineLoaderNetworkOnly.withCache(
+          cache = cache,
+        ).withEmbedded(
+          embeddedDir = embeddedDir,
+          embeddedFileSystem = embeddedFileSystem,
+        )
+      }
     }
 
     val manifestUrlFlowForLoad = when (spec.freshCodePolicy) {
diff --git a/redwood-treehouse/src/jvmMain/kotlin/app/cash/redwood/treehouse/FrameClockServiceJvm.kt b/redwood-treehouse/src/jvmMain/kotlin/app/cash/redwood/treehouse/FrameClockServiceJvm.kt
deleted file mode 100644
index a893e52f4..000000000
--- a/redwood-treehouse/src/jvmMain/kotlin/app/cash/redwood/treehouse/FrameClockServiceJvm.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2022 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.treehouse
-
-// TODO(jwilson): deduplicate with identical symbol in hostMain
-public actual val StandardFrameClockService: FrameClockService = object : FrameClockService {
-  override fun sendFrame(timeNanos: Long) {
-    error("unexpected call to sendFrame() on the host")
-  }
-}


Test patch:
diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
index 610ccc11f..87e796ce4 100644
--- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
+++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
@@ -21,7 +21,7 @@ import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import app.cash.redwood.compose.WidgetVersion
 import app.cash.redwood.protocol.ChildrenDiff
-import app.cash.redwood.protocol.ChildrenDiff.Companion.RootChildrenTag
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.Diff
 import app.cash.redwood.protocol.Event
 import app.cash.redwood.protocol.EventTag
@@ -86,10 +86,10 @@ class ProtocolTest {
     assertEquals(
       Diff(
         childrenDiffs = listOf(
-          ChildrenDiff.Insert(Id.Root, RootChildrenTag, Id(1), 1 /* row */, 0),
-          ChildrenDiff.Insert(Id(1), 1U, Id(2), 3 /* text */, 0),
-          ChildrenDiff.Insert(Id(1), 1U, Id(3), 1 /* row */, 1),
-          ChildrenDiff.Insert(Id(3), 1U, Id(4), 3 /* text */, 0),
+          ChildrenDiff.Insert(Id.Root, ChildrenTag.Root, Id(1), 1 /* row */, 0),
+          ChildrenDiff.Insert(Id(1), ChildrenTag(1), Id(2), 3 /* text */, 0),
+          ChildrenDiff.Insert(Id(1), ChildrenTag(1), Id(3), 1 /* row */, 1),
+          ChildrenDiff.Insert(Id(3), ChildrenTag(1), Id(4), 3 /* text */, 0),
         ),
         layoutModifiers = listOf(
           LayoutModifiers(Id(1), JsonArray(listOf())),
@@ -137,7 +137,7 @@ class ProtocolTest {
     assertEquals(
       Diff(
         childrenDiffs = listOf(
-          ChildrenDiff.Insert(Id.Root, RootChildrenTag, Id(1), 4 /* button */, 0),
+          ChildrenDiff.Insert(Id.Root, ChildrenTag.Root, Id(1), 4 /* button */, 0),
         ),
         layoutModifiers = listOf(
           LayoutModifiers(Id(1), JsonArray(listOf())),
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
index 8cb6c8e65..a4340f503 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
@@ -16,6 +16,7 @@
 package app.cash.redwood.protocol.widget
 
 import app.cash.redwood.LayoutModifier
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.Event
 import app.cash.redwood.protocol.EventSink
 import app.cash.redwood.protocol.EventTag
@@ -197,7 +198,7 @@ class DiffConsumingWidgetFactoryTest {
     val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
 
     val t = assertFailsWith<IllegalArgumentException> {
-      button.children(345432U)
+      button.children(ChildrenTag(345432))
     }
     assertEquals("Unknown children tag 345432 for widget kind 4", t.message)
   }
@@ -210,7 +211,7 @@ class DiffConsumingWidgetFactoryTest {
     )
 
     val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
-    assertNull(button.children(345432U))
+    assertNull(button.children(ChildrenTag(345432)))
 
     assertEquals("Unknown children 345432 for 4", handler.events.single())
   }
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt
index 852cd5f85..ec634940f 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt
@@ -16,7 +16,7 @@
 package app.cash.redwood.protocol.widget
 
 import app.cash.redwood.protocol.ChildrenDiff
-import app.cash.redwood.protocol.ChildrenDiff.Companion.RootChildrenTag
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.Diff
 import app.cash.redwood.protocol.Id
 import app.cash.redwood.widget.MutableListChildren
@@ -36,7 +36,7 @@ class ProtocolBridgeTest {
       childrenDiffs = listOf(
         ChildrenDiff.Insert(
           id = Id.Root,
-          tag = RootChildrenTag,
+          tag = ChildrenTag.Root,
           childId = Id.Root,
           kind = 4 /* button */,
           index = 0,
@@ -59,7 +59,7 @@ class ProtocolBridgeTest {
       childrenDiffs = listOf(
         ChildrenDiff.Insert(
           id = Id.Root,
-          tag = RootChildrenTag,
+          tag = ChildrenTag.Root,
           childId = Id(1),
           kind = 4 /* button */,
           index = 0,
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandlerTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandlerTest.kt
index faa52b1b2..0b8dff49e 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandlerTest.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandlerTest.kt
@@ -15,6 +15,7 @@
  */
 package app.cash.redwood.protocol.widget
 
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.PropertyTag
 import kotlin.test.Test
 import kotlin.test.assertEquals
@@ -37,7 +38,7 @@ class ProtocolMismatchHandlerTest {
 
   @Test fun throwingUnknownChildren() {
     val t = assertFailsWith<IllegalArgumentException> {
-      ProtocolMismatchHandler.Throwing.onUnknownChildren(1, 2U)
+      ProtocolMismatchHandler.Throwing.onUnknownChildren(1, ChildrenTag(2))
     }
     assertEquals("Unknown children tag 2 for widget kind 1", t.message)
   }
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt
index e7e177c18..3e2d6c1a0 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt
@@ -15,6 +15,7 @@
  */
 package app.cash.redwood.protocol.widget
 
+import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.PropertyTag
 
 class RecordingProtocolMismatchHandler : ProtocolMismatchHandler {
@@ -28,8 +29,8 @@ class RecordingProtocolMismatchHandler : ProtocolMismatchHandler {
     events += "Unknown layout modifier $tag"
   }
 
-  override fun onUnknownChildren(kind: Int, tag: UInt) {
-    events += "Unknown children $tag for $kind"
+  override fun onUnknownChildren(kind: Int, tag: ChildrenTag) {
+    events += "Unknown children ${tag.value} for $kind"
   }
 
   override fun onUnknownProperty(kind: Int, tag: PropertyTag) {
diff --git a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt
index 7bcf7e3b9..3eedbcc42 100644
--- a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt
+++ b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt
@@ -44,9 +44,9 @@ class ProtocolTest {
   @Test fun diff() {
     val model = Diff(
       childrenDiffs = listOf(
-        ChildrenDiff.Insert(Id(1), 2U, Id(3), 4, 5),
-        ChildrenDiff.Move(Id(1), 2U, 3, 4, 5),
-        ChildrenDiff.Remove(Id(1), 2U, 3, 4),
+        ChildrenDiff.Insert(Id(1), ChildrenTag(2), Id(3), 4, 5),
+        ChildrenDiff.Move(Id(1), ChildrenTag(2), 3, 4, 5),
+        ChildrenDiff.Remove(Id(1), ChildrenTag(2), 3, 4),
       ),
       layoutModifiers = listOf(
         LayoutModifiers(