From a743d06154879dada493f4f6280650496de046f9 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Thu, 4 Apr 2024 09:54:46 -0400 Subject: [PATCH] Create RedwoodVersion type for sharing across host-guest bridge (#1929) In order to work around bugs in the future or change the protocol in a backwards-incompatible way, the version gives the other side of the bridge the ability to change itself to remain compatible. For now, we only have the host version hooked up to the guest side. The guest version being exposed to the host will come in a follow-up. --- CHANGELOG.md | 1 + .../redwood/compose/ChangeListenerTest.kt | 15 ++- .../api/android/redwood-protocol-guest.api | 8 +- .../api/jvm/redwood-protocol-guest.api | 8 +- redwood-protocol-guest/build.gradle | 15 +++ .../redwood/protocol/guest/ProtocolBridge.kt | 2 + .../guest/GeneratedProtocolBridgeTest.kt | 46 ++++++-- .../protocol/guest/GuestVersionTest.kt | 25 +++++ .../redwood/protocol/guest/ProtocolTest.kt | 5 +- .../api/android/redwood-protocol-host.api | 6 +- .../api/jvm/redwood-protocol-host.api | 6 +- redwood-protocol-host/build.gradle | 15 +++ .../redwood/protocol/host/ProtocolBridge.kt | 3 + .../redwood/protocol/host/HostVersionTest.kt | 25 +++++ .../protocol/host/ProtocolBridgeTest.kt | 6 + redwood-protocol/api/redwood-protocol.api | 34 ++++++ .../cash/redwood/protocol/RedwoodVersion.kt | 96 ++++++++++++++++ .../redwood/protocol/RedwoodVersionTest.kt | 106 ++++++++++++++++++ .../app/cash/redwood/testing/toChangeList.kt | 7 +- .../redwood/testing/ViewRecyclingTester.kt | 7 +- .../app/cash/redwood/testing/ViewTreesTest.kt | 15 ++- .../codegen/protocolGuestGeneration.kt | 2 + .../app/cash/redwood/tooling/codegen/types.kt | 1 + .../redwood/treehouse/StandardAppLifecycle.kt | 10 ++ .../redwood/treehouse/treehouseCompose.kt | 7 +- .../redwood/treehouse/ChangeListRenderer.kt | 3 + .../redwood/treehouse/RealAppLifecycleHost.kt | 3 + .../redwood/treehouse/TreehouseAppContent.kt | 3 + .../api/android/redwood-treehouse.api | 1 + .../api/jvm/redwood-treehouse.api | 1 + redwood-treehouse/api/zipline-api.toml | 3 + .../cash/redwood/treehouse/AppLifecycle.kt | 8 ++ 32 files changed, 466 insertions(+), 27 deletions(-) create mode 100644 redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/GuestVersionTest.kt create mode 100644 redwood-protocol-host/src/commonTest/kotlin/app/cash/redwood/protocol/host/HostVersionTest.kt create mode 100644 redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/RedwoodVersion.kt create mode 100644 redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/RedwoodVersionTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cfccf4ebc..867b1e1707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Changed: - Protocol host code is now under `your.package.protocol.host`. - The 'app.cash.redwood.generator.compose.protocol' and 'app.cash.redwood.generator.widget.protocol' Gradle plugins are now deprecated and will be removed in the next release. Use 'app.cash.redwood.generator.protocol.guest' and 'app.cash.redwood.generator.protocol.host', respectively. - The 'redwood-tooling-codegen' CLI flags for protocol codegen have changed from `--compose-protocol` and `--widget-protocol` to `--protocol-guest` and `--protocol-host`, respectively. +- Entrypoints to the protocol on the host-side and guest-side now require supplying the version of Redwood in use on the other side in order to ensure compatibility and work around any bugs in older versions. This uses a new `RedwoodVersion` type, and will be automatically wired if using our Treehouse artifacts. Fixed: - Fix failure to release JS resources when calling `CoroutineScope` is being cancelled diff --git a/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ChangeListenerTest.kt b/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ChangeListenerTest.kt index 34c437d044..fb5cc9a5cf 100644 --- a/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ChangeListenerTest.kt +++ b/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/ChangeListenerTest.kt @@ -22,7 +22,9 @@ import app.cash.redwood.Modifier import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.layout.testing.RedwoodLayoutTestingWidgetFactory import app.cash.redwood.lazylayout.testing.RedwoodLazyLayoutTestingWidgetFactory +import app.cash.redwood.protocol.guest.guestRedwoodVersion import app.cash.redwood.protocol.host.ProtocolBridge +import app.cash.redwood.protocol.host.hostRedwoodVersion import app.cash.redwood.testing.TestRedwoodComposition import app.cash.redwood.testing.WidgetValue import app.cash.redwood.widget.MutableListChildren @@ -55,10 +57,15 @@ class ProtocolChangeListenerTest : AbstractChangeListenerTest() { widgetSystem: TestSchemaWidgetSystem, snapshot: () -> T, ): TestRedwoodComposition { - val composeBridge = TestSchemaProtocolBridge.create() - val widgetBridge = ProtocolBridge(MutableListChildren(), TestSchemaProtocolFactory(widgetSystem)) { - throw AssertionError() - } + val composeBridge = TestSchemaProtocolBridge.create( + hostVersion = hostRedwoodVersion, + ) + val widgetBridge = ProtocolBridge( + guestVersion = guestRedwoodVersion, + container = MutableListChildren(), + factory = TestSchemaProtocolFactory(widgetSystem), + eventSink = { throw AssertionError() }, + ) return TestRedwoodComposition(this, composeBridge.widgetSystem, composeBridge.root) { composeBridge.getChangesOrNull()?.let { changes -> widgetBridge.sendChanges(changes) diff --git a/redwood-protocol-guest/api/android/redwood-protocol-guest.api b/redwood-protocol-guest/api/android/redwood-protocol-guest.api index 67de3a006f..87d85039a0 100644 --- a/redwood-protocol-guest/api/android/redwood-protocol-guest.api +++ b/redwood-protocol-guest/api/android/redwood-protocol-guest.api @@ -5,8 +5,8 @@ public abstract interface class app/cash/redwood/protocol/guest/ProtocolBridge : } public abstract interface class app/cash/redwood/protocol/guest/ProtocolBridge$Factory { - public abstract fun create (Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/protocol/guest/ProtocolBridge; - public static synthetic fun create$default (Lapp/cash/redwood/protocol/guest/ProtocolBridge$Factory;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/protocol/guest/ProtocolBridge; + public abstract fun create-Bvskwvs (Ljava/lang/String;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/protocol/guest/ProtocolBridge; + public static synthetic fun create-Bvskwvs$default (Lapp/cash/redwood/protocol/guest/ProtocolBridge$Factory;Ljava/lang/String;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/protocol/guest/ProtocolBridge; } public abstract interface class app/cash/redwood/protocol/guest/ProtocolMismatchHandler { @@ -54,3 +54,7 @@ public final class app/cash/redwood/protocol/guest/ProtocolWidgetChildren : app/ public final fun visitIds (Lkotlin/jvm/functions/Function1;)V } +public final class app/cash/redwood/protocol/guest/VersionKt { + public static final fun getGuestRedwoodVersion ()Ljava/lang/String; +} + diff --git a/redwood-protocol-guest/api/jvm/redwood-protocol-guest.api b/redwood-protocol-guest/api/jvm/redwood-protocol-guest.api index 67de3a006f..87d85039a0 100644 --- a/redwood-protocol-guest/api/jvm/redwood-protocol-guest.api +++ b/redwood-protocol-guest/api/jvm/redwood-protocol-guest.api @@ -5,8 +5,8 @@ public abstract interface class app/cash/redwood/protocol/guest/ProtocolBridge : } public abstract interface class app/cash/redwood/protocol/guest/ProtocolBridge$Factory { - public abstract fun create (Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/protocol/guest/ProtocolBridge; - public static synthetic fun create$default (Lapp/cash/redwood/protocol/guest/ProtocolBridge$Factory;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/protocol/guest/ProtocolBridge; + public abstract fun create-Bvskwvs (Ljava/lang/String;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/protocol/guest/ProtocolBridge; + public static synthetic fun create-Bvskwvs$default (Lapp/cash/redwood/protocol/guest/ProtocolBridge$Factory;Ljava/lang/String;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/protocol/guest/ProtocolBridge; } public abstract interface class app/cash/redwood/protocol/guest/ProtocolMismatchHandler { @@ -54,3 +54,7 @@ public final class app/cash/redwood/protocol/guest/ProtocolWidgetChildren : app/ public final fun visitIds (Lkotlin/jvm/functions/Function1;)V } +public final class app/cash/redwood/protocol/guest/VersionKt { + public static final fun getGuestRedwoodVersion ()Ljava/lang/String; +} + diff --git a/redwood-protocol-guest/build.gradle b/redwood-protocol-guest/build.gradle index 2b7125ac2c..f24f5b8900 100644 --- a/redwood-protocol-guest/build.gradle +++ b/redwood-protocol-guest/build.gradle @@ -3,6 +3,7 @@ import app.cash.redwood.buildsupport.KmpTargets apply plugin: 'org.jetbrains.kotlin.multiplatform' apply plugin: 'com.android.library' +apply plugin: 'com.github.gmazzo.buildconfig' redwoodBuild { composeCompiler() @@ -52,3 +53,17 @@ android { unitTests.returnDefaultValues = true } } + +buildConfig { + useKotlinOutput { + topLevelConstants = true + } + + className("Version") + packageName('app.cash.redwood.protocol.guest') + buildConfigField( + "app.cash.redwood.protocol.RedwoodVersion", + "guestRedwoodVersion", + "RedwoodVersion(\"${project.version}\")", + ) +} diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolBridge.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolBridge.kt index 709e4ca8de..ad99873638 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolBridge.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolBridge.kt @@ -20,6 +20,7 @@ import app.cash.redwood.protocol.ChildrenTag import app.cash.redwood.protocol.Event import app.cash.redwood.protocol.EventSink import app.cash.redwood.protocol.Id +import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.widget.Widget import app.cash.redwood.widget.WidgetSystem import kotlinx.serialization.json.Json @@ -52,6 +53,7 @@ public interface ProtocolBridge : EventSink { public interface Factory { /** Create a new [ProtocolBridge] with its own protocol state and set of tracked widgets. */ public fun create( + hostVersion: RedwoodVersion, json: Json = Json.Default, mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing, ): ProtocolBridge diff --git a/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/GeneratedProtocolBridgeTest.kt b/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/GeneratedProtocolBridgeTest.kt index 5fccc6939e..4f48694094 100644 --- a/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/GeneratedProtocolBridgeTest.kt +++ b/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/GeneratedProtocolBridgeTest.kt @@ -49,7 +49,11 @@ class GeneratedProtocolBridgeTest { contextual(Duration::class, DurationIsoSerializer) } } - val bridge = TestSchemaProtocolBridge.create(json) + val bridge = TestSchemaProtocolBridge.create( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + json = json, + ) val textInput = bridge.widgetSystem.TestSchema.TextInput() textInput.customType(10.seconds) @@ -67,7 +71,11 @@ class GeneratedProtocolBridgeTest { contextual(Duration::class, DurationIsoSerializer) } } - val bridge = TestSchemaProtocolBridge.create(json) + val bridge = TestSchemaProtocolBridge.create( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + json = json, + ) val button = bridge.widgetSystem.TestSchema.Button() button.modifier = with(object : TestScope {}) { @@ -97,7 +105,11 @@ class GeneratedProtocolBridgeTest { contextual(Duration::class, DurationIsoSerializer) } } - val bridge = TestSchemaProtocolBridge.create(json) + val bridge = TestSchemaProtocolBridge.create( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + json = json, + ) val button = bridge.widgetSystem.TestSchema.Button() button.modifier = with(object : TestScope {}) { @@ -127,7 +139,11 @@ class GeneratedProtocolBridgeTest { contextual(Duration::class, DurationIsoSerializer) } } - val bridge = TestSchemaProtocolBridge.create(json) + val bridge = TestSchemaProtocolBridge.create( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + json = json, + ) val textInput = bridge.widgetSystem.TestSchema.TextInput() val protocolWidget = textInput as ProtocolWidget @@ -143,7 +159,10 @@ class GeneratedProtocolBridgeTest { } @Test fun unknownEventThrowsDefault() { - val bridge = TestSchemaProtocolBridge.create() + val bridge = TestSchemaProtocolBridge.create( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + ) val button = bridge.widgetSystem.TestSchema.Button() as ProtocolWidget val t = assertFailsWith { @@ -155,7 +174,11 @@ class GeneratedProtocolBridgeTest { @Test fun unknownEventCallsHandler() { val handler = RecordingProtocolMismatchHandler() - val bridge = TestSchemaProtocolBridge.create(mismatchHandler = handler) + val bridge = TestSchemaProtocolBridge.create( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + mismatchHandler = handler, + ) val button = bridge.widgetSystem.TestSchema.Button() as ProtocolWidget button.sendEvent(Event(Id(1), EventTag(3456543))) @@ -164,7 +187,10 @@ class GeneratedProtocolBridgeTest { } @Test fun unknownEventNodeThrowsDefault() { - val bridge = TestSchemaProtocolBridge.create() + val bridge = TestSchemaProtocolBridge.create( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + ) val t = assertFailsWith { bridge.sendEvent(Event(Id(3456543), EventTag(1))) } @@ -173,7 +199,11 @@ class GeneratedProtocolBridgeTest { @Test fun unknownEventNodeCallsHandler() { val handler = RecordingProtocolMismatchHandler() - val bridge = TestSchemaProtocolBridge.create(mismatchHandler = handler) + val bridge = TestSchemaProtocolBridge.create( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + mismatchHandler = handler, + ) bridge.sendEvent(Event(Id(3456543), EventTag(1))) diff --git a/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/GuestVersionTest.kt b/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/GuestVersionTest.kt new file mode 100644 index 0000000000..afad427160 --- /dev/null +++ b/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/GuestVersionTest.kt @@ -0,0 +1,25 @@ +/* + * 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.guest + +import kotlin.test.Test +import kotlin.test.assertNotNull + +class GuestVersionTest { + @Test fun parses() { + assertNotNull(guestRedwoodVersion) + } +} diff --git a/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/ProtocolTest.kt b/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/ProtocolTest.kt index b7bfc890c3..55df7fc2f8 100644 --- a/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/ProtocolTest.kt +++ b/redwood-protocol-guest/src/commonTest/kotlin/app/cash/redwood/protocol/guest/ProtocolTest.kt @@ -58,7 +58,10 @@ import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.JsonPrimitive class ProtocolTest { - private val bridge = TestSchemaProtocolBridge.create() + private val bridge = TestSchemaProtocolBridge.create( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + ) @Test fun widgetVersionPropagated() = runTest { val composition = ProtocolRedwoodComposition( diff --git a/redwood-protocol-host/api/android/redwood-protocol-host.api b/redwood-protocol-host/api/android/redwood-protocol-host.api index 8253728813..68aa8dbcc5 100644 --- a/redwood-protocol-host/api/android/redwood-protocol-host.api +++ b/redwood-protocol-host/api/android/redwood-protocol-host.api @@ -5,7 +5,7 @@ public abstract interface class app/cash/redwood/protocol/host/GeneratedProtocol } public final class app/cash/redwood/protocol/host/ProtocolBridge : app/cash/redwood/protocol/ChangesSink { - public fun (Lapp/cash/redwood/widget/Widget$Children;Lapp/cash/redwood/protocol/host/ProtocolFactory;Lapp/cash/redwood/protocol/EventSink;)V + public synthetic fun (Ljava/lang/String;Lapp/cash/redwood/widget/Widget$Children;Lapp/cash/redwood/protocol/host/ProtocolFactory;Lapp/cash/redwood/protocol/EventSink;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun sendChanges (Ljava/util/List;)V } @@ -42,3 +42,7 @@ public abstract class app/cash/redwood/protocol/host/ProtocolNode { public abstract fun visitIds (Lkotlin/jvm/functions/Function1;)V } +public final class app/cash/redwood/protocol/host/VersionKt { + public static final fun getHostRedwoodVersion ()Ljava/lang/String; +} + diff --git a/redwood-protocol-host/api/jvm/redwood-protocol-host.api b/redwood-protocol-host/api/jvm/redwood-protocol-host.api index 8253728813..68aa8dbcc5 100644 --- a/redwood-protocol-host/api/jvm/redwood-protocol-host.api +++ b/redwood-protocol-host/api/jvm/redwood-protocol-host.api @@ -5,7 +5,7 @@ public abstract interface class app/cash/redwood/protocol/host/GeneratedProtocol } public final class app/cash/redwood/protocol/host/ProtocolBridge : app/cash/redwood/protocol/ChangesSink { - public fun (Lapp/cash/redwood/widget/Widget$Children;Lapp/cash/redwood/protocol/host/ProtocolFactory;Lapp/cash/redwood/protocol/EventSink;)V + public synthetic fun (Ljava/lang/String;Lapp/cash/redwood/widget/Widget$Children;Lapp/cash/redwood/protocol/host/ProtocolFactory;Lapp/cash/redwood/protocol/EventSink;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun sendChanges (Ljava/util/List;)V } @@ -42,3 +42,7 @@ public abstract class app/cash/redwood/protocol/host/ProtocolNode { public abstract fun visitIds (Lkotlin/jvm/functions/Function1;)V } +public final class app/cash/redwood/protocol/host/VersionKt { + public static final fun getHostRedwoodVersion ()Ljava/lang/String; +} + diff --git a/redwood-protocol-host/build.gradle b/redwood-protocol-host/build.gradle index b02a01bd4e..7be6b661cc 100644 --- a/redwood-protocol-host/build.gradle +++ b/redwood-protocol-host/build.gradle @@ -3,6 +3,7 @@ import app.cash.redwood.buildsupport.KmpTargets apply plugin: 'org.jetbrains.kotlin.multiplatform' apply plugin: 'com.android.library' +apply plugin: 'com.github.gmazzo.buildconfig' redwoodBuild { publishing() @@ -34,3 +35,17 @@ kotlin { android { namespace 'app.cash.redwood.protocol.widget' } + +buildConfig { + useKotlinOutput { + topLevelConstants = true + } + + className("Version") + packageName('app.cash.redwood.protocol.host') + buildConfigField( + "app.cash.redwood.protocol.RedwoodVersion", + "hostRedwoodVersion", + "RedwoodVersion(\"${project.version}\")", + ) +} diff --git a/redwood-protocol-host/src/commonMain/kotlin/app/cash/redwood/protocol/host/ProtocolBridge.kt b/redwood-protocol-host/src/commonMain/kotlin/app/cash/redwood/protocol/host/ProtocolBridge.kt index 7a84a8973b..90de5826ae 100644 --- a/redwood-protocol-host/src/commonMain/kotlin/app/cash/redwood/protocol/host/ProtocolBridge.kt +++ b/redwood-protocol-host/src/commonMain/kotlin/app/cash/redwood/protocol/host/ProtocolBridge.kt @@ -29,6 +29,7 @@ import app.cash.redwood.protocol.EventSink import app.cash.redwood.protocol.Id import app.cash.redwood.protocol.ModifierChange import app.cash.redwood.protocol.PropertyChange +import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.protocol.WidgetTag import app.cash.redwood.widget.ChangeListener import app.cash.redwood.widget.Widget @@ -44,6 +45,8 @@ import kotlin.native.ObjCName @OptIn(RedwoodCodegenApi::class) @ObjCName("ProtocolBridge", exact = true) public class ProtocolBridge( + @Suppress("UNUSED_PARAMETER") + guestVersion: RedwoodVersion, container: Widget.Children, factory: ProtocolFactory, private val eventSink: EventSink, diff --git a/redwood-protocol-host/src/commonTest/kotlin/app/cash/redwood/protocol/host/HostVersionTest.kt b/redwood-protocol-host/src/commonTest/kotlin/app/cash/redwood/protocol/host/HostVersionTest.kt new file mode 100644 index 0000000000..15e7b5203a --- /dev/null +++ b/redwood-protocol-host/src/commonTest/kotlin/app/cash/redwood/protocol/host/HostVersionTest.kt @@ -0,0 +1,25 @@ +/* + * 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 kotlin.test.Test +import kotlin.test.assertNotNull + +class HostVersionTest { + @Test fun parses() { + assertNotNull(hostRedwoodVersion) + } +} diff --git a/redwood-protocol-host/src/commonTest/kotlin/app/cash/redwood/protocol/host/ProtocolBridgeTest.kt b/redwood-protocol-host/src/commonTest/kotlin/app/cash/redwood/protocol/host/ProtocolBridgeTest.kt index 772c043b4a..092eec71be 100644 --- a/redwood-protocol-host/src/commonTest/kotlin/app/cash/redwood/protocol/host/ProtocolBridgeTest.kt +++ b/redwood-protocol-host/src/commonTest/kotlin/app/cash/redwood/protocol/host/ProtocolBridgeTest.kt @@ -27,6 +27,7 @@ import app.cash.redwood.protocol.ModifierChange import app.cash.redwood.protocol.PropertyChange import app.cash.redwood.protocol.PropertyTag import app.cash.redwood.protocol.WidgetTag +import app.cash.redwood.protocol.guest.guestRedwoodVersion import app.cash.redwood.widget.MutableListChildren import assertk.assertFailure import assertk.assertThat @@ -45,6 +46,7 @@ import kotlinx.serialization.json.JsonPrimitive class ProtocolBridgeTest { @Test fun createRootIdThrows() { val bridge = ProtocolBridge( + guestVersion = guestRedwoodVersion, container = MutableListChildren(), factory = TestSchemaProtocolFactory( widgetSystem = TestSchemaWidgetSystem( @@ -70,6 +72,7 @@ class ProtocolBridgeTest { @Test fun duplicateIdThrows() { val bridge = ProtocolBridge( + guestVersion = guestRedwoodVersion, container = MutableListChildren(), factory = TestSchemaProtocolFactory( widgetSystem = TestSchemaWidgetSystem( @@ -96,6 +99,7 @@ class ProtocolBridgeTest { @Test fun removeRemoves() { val bridge = ProtocolBridge( + guestVersion = guestRedwoodVersion, container = MutableListChildren(), factory = TestSchemaProtocolFactory( widgetSystem = TestSchemaWidgetSystem( @@ -155,6 +159,7 @@ class ProtocolBridgeTest { @Test fun modifierChangeNotifiesContainer() { var modifierUpdateCount = 0 val bridge = ProtocolBridge( + guestVersion = guestRedwoodVersion, container = MutableListChildren(modifierUpdated = { modifierUpdateCount++ }), factory = TestSchemaProtocolFactory( widgetSystem = TestSchemaWidgetSystem( @@ -188,6 +193,7 @@ class ProtocolBridgeTest { @Test fun entireSubtreeRemoved() { val bridge = ProtocolBridge( + guestVersion = guestRedwoodVersion, container = MutableListChildren(), factory = TestSchemaProtocolFactory( widgetSystem = TestSchemaWidgetSystem( diff --git a/redwood-protocol/api/redwood-protocol.api b/redwood-protocol/api/redwood-protocol.api index 33c87116a6..2b1858c638 100644 --- a/redwood-protocol/api/redwood-protocol.api +++ b/redwood-protocol/api/redwood-protocol.api @@ -374,6 +374,40 @@ public final class app/cash/redwood/protocol/PropertyTag$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class app/cash/redwood/protocol/RedwoodVersion : java/lang/Comparable { + public static final field Companion Lapp/cash/redwood/protocol/RedwoodVersion$Companion; + public static final synthetic fun box-impl (Ljava/lang/String;)Lapp/cash/redwood/protocol/RedwoodVersion; + public synthetic fun compareTo (Ljava/lang/Object;)I + public fun compareTo-lbpnSQA (Ljava/lang/String;)I + public static fun compareTo-lbpnSQA (Ljava/lang/String;Ljava/lang/String;)I + public static fun constructor-impl (Ljava/lang/String;)Ljava/lang/String; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z + public final fun getValue ()Ljava/lang/String; + public fun hashCode ()I + public static fun hashCode-impl (Ljava/lang/String;)I + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Ljava/lang/String;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Ljava/lang/String; +} + +public final class app/cash/redwood/protocol/RedwoodVersion$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lapp/cash/redwood/protocol/RedwoodVersion$$serializer; + public fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize-lfoQLWY (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/String; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize-E69qDtc (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/String;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class app/cash/redwood/protocol/RedwoodVersion$Companion { + public final fun getUnknown-7jYel6c ()Ljava/lang/String; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class app/cash/redwood/protocol/SnapshotChangeList { public static final field Companion Lapp/cash/redwood/protocol/SnapshotChangeList$Companion; public static final synthetic fun box-impl (Ljava/util/List;)Lapp/cash/redwood/protocol/SnapshotChangeList; diff --git a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/RedwoodVersion.kt b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/RedwoodVersion.kt new file mode 100644 index 0000000000..6b5bc0adc5 --- /dev/null +++ b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/RedwoodVersion.kt @@ -0,0 +1,96 @@ +/* + * 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 + +import kotlin.jvm.JvmInline +import kotlinx.serialization.Serializable + +/** + * Version string for the Redwood project. This is a very strict subset of + * [Gradle's version parsing and ordering](https://docs.gradle.org/current/userguide/single_versions.html#version_ordering) + * semantics. + * + * Format is `X.Y.Z[-label]` (with square brackets indicating an optional part). `X`, `Y`, and `Z` + * are integers and separated by a period (`.`). `label` is an optional string delimited by a + * hyphen (`-`) and whose valid characters are a-z, A-Z, 0-9, period (`.`), underscore (`_`), + * and hyphen (`-`). + * + * Ordering relative to another version string is done by comparing `X` numerically, then `Y` + * numerically, then `Z` numerically, and finally by comparing `label` lexicographically. The label + * value "SNAPSHOT" always sorting higher than any other label. The absence of a label sorts higher + * than any present label. + * + * Examples: + * - 2.1.0 > 1.2.3 (`X` numerically higher) + * - 1.3.1 > 1.2.0 (`X` same, `Y` numerically higher) + * - 1.1.4 > 1.1.2 (`X` and `Y` same, `Z` numerically higher) + * - 1.0.0 > 1.0.0-beta (`X`, `Y`, and `Z` same, absent label higher than present label) + * - 1.0.0-SNAPSHOT > 1.0.0-beta (`X`, `Y`, and `Z` same, "SNAPSHOT" label higher than any label) + * - 1.0.0-beta > 1.0.0-alpha (`X`, `Y`, and `Z` same, "beta" lexicographically higher than "alpha") + * - 1.0.0-beta2 > 1.0.0-beta (`X`, `Y`, and `Z` same, "beta2" lexicographically higher than "beta") + */ +@JvmInline +@Serializable +public value class RedwoodVersion(public val value: String) : Comparable { + init { + require(format.matches(value)) { + "Invalid version format: $value" + } + } + + /** + * Compare two versions to see which is newer. + * + * Note: Comparing instances is not particularly efficient, so the result should be cached + * instead of comparing each time. + */ + override fun compareTo(other: RedwoodVersion): Int { + val thisMatch = format.matchEntire(value)!! + val otherMatch = format.matchEntire(other.value)!! + + // First three parts are digits (1-indexed because part 0 is whole match). + for (i in 1..3) { + val thisPart = thisMatch.groups[i]!!.value.toInt() + val otherPart = otherMatch.groups[i]!!.value.toInt() + if (thisPart > otherPart) return 1 + if (thisPart < otherPart) return -1 + } + + val thisLabel = thisMatch.groups[4] + val otherLabel = otherMatch.groups[4] + + // The absence of a label sorts higher than the presence of one. + if (thisLabel == null) { + return if (otherLabel == null) 0 else 1 + } else if (otherLabel == null) { + return -1 + } + + // SNAPSHOT label sorts higher than any other label. + if (thisLabel.value == "-SNAPSHOT") { + return if (otherLabel.value == "-SNAPSHOT") 0 else 1 + } else if (otherLabel.value == "-SNAPSHOT") { + return -1 + } + + return thisLabel.value.compareTo(otherLabel.value) + } + + public companion object { + private val format = Regex("""^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[a-zA-Z0-9._-]+)?$""") + public val Unknown: RedwoodVersion = RedwoodVersion("0.0.0") + } +} diff --git a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/RedwoodVersionTest.kt b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/RedwoodVersionTest.kt new file mode 100644 index 0000000000..1498641f0c --- /dev/null +++ b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/RedwoodVersionTest.kt @@ -0,0 +1,106 @@ +/* + * 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 + +import assertk.assertAll +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isGreaterThan +import assertk.assertions.isInstanceOf +import assertk.assertions.isLessThan +import assertk.assertions.message +import kotlin.test.Test + +class RedwoodVersionTest { + @Test fun parsing() { + // Too few numbers. + assertInvalidVersion("1") + assertInvalidVersion("1-beta") + assertInvalidVersion("1.1") + assertInvalidVersion("1.1-beta") + + // Trailing dots. + assertInvalidVersion("1.") + assertInvalidVersion("1.1.") + assertInvalidVersion("1.1.1.") + + // Non-digit numbers. + assertInvalidVersion("a.1.1") + assertInvalidVersion("1.a.1") + assertInvalidVersion("1.1.a") + + // Too many numbers. + assertInvalidVersion("1.0.0.0") + assertInvalidVersion("1.0.0.0-beta") + + // Leading zeros. + assertInvalidVersion("01.1.1") + assertInvalidVersion("1.01.1") + assertInvalidVersion("1.1.01") + + // Trailing label separator. + assertInvalidVersion("1-") + assertInvalidVersion("1.0-") + assertInvalidVersion("1.0.0-") + assertInvalidVersion("1.0.0.0-") + + // Invalid label characters. + assertInvalidVersion("1.0.0-h∑y") + } + + private fun assertInvalidVersion(version: String) { + assertFailure { RedwoodVersion(version) } + .isInstanceOf() + .message() + .isEqualTo("Invalid version format: $version") + } + + @Test fun ordering() { + // Examples from docs. + assertVersionOrdering("2.1.0", "1.2.3") + assertVersionOrdering("1.3.1", "1.2.0") + assertVersionOrdering("1.1.4", "1.1.2") + assertVersionOrdering("1.0.0", "1.0.0-beta") + assertVersionOrdering("1.0.0-SNAPSHOT", "1.0.0-beta") + assertVersionOrdering("1.0.0-beta", "1.0.0-alpha") + assertVersionOrdering("1.0.0-beta2", "1.0.0-beta") + + // Multi-digit examples. + assertVersionOrdering("20.0.0", "1.0.0") + assertVersionOrdering("20.0.0", "10.0.0") + assertVersionOrdering("1.20.0", "1.1.0") + assertVersionOrdering("1.20.0", "1.10.0") + assertVersionOrdering("1.1.20", "1.1.0") + assertVersionOrdering("1.1.20", "1.1.10") + + // Label sorting. + assertVersionOrdering("1.0.0-z", "1.0.0-a") + assertVersionOrdering("1.0.0-az", "1.0.0-a") + assertVersionOrdering("1.0.0-beta2", "1.0.0-beta10") + assertVersionOrdering("1.0.0-beta10", "1.0.0-beta02") + assertVersionOrdering("1.0.0-beta20", "1.0.0-beta10") + } + + private fun assertVersionOrdering(newerVersion: String, olderVersion: String) { + val newer = RedwoodVersion(newerVersion) + val older = RedwoodVersion(olderVersion) + assertAll { + assertThat(newer).isGreaterThan(older) + assertThat(older).isLessThan(newer) + } + } +} diff --git a/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/toChangeList.kt b/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/toChangeList.kt index 5ee3f296ec..f861da8e10 100644 --- a/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/toChangeList.kt +++ b/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/toChangeList.kt @@ -17,6 +17,7 @@ package app.cash.redwood.testing import app.cash.redwood.protocol.SnapshotChangeList import app.cash.redwood.protocol.guest.ProtocolBridge +import app.cash.redwood.protocol.guest.guestRedwoodVersion import kotlinx.serialization.json.Json /** @@ -27,7 +28,11 @@ public fun List.toChangeList( factory: ProtocolBridge.Factory, json: Json = Json.Default, ): SnapshotChangeList { - val bridge = factory.create(json) + val bridge = factory.create( + // Use latest guest version as the host version to avoid any compatibility behavior. + hostVersion = guestRedwoodVersion, + json = json, + ) for ((index, child) in withIndex()) { bridge.root.insert(index, child.toWidget(bridge.widgetSystem)) } diff --git a/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewRecyclingTester.kt b/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewRecyclingTester.kt index 3702acee36..4d29426d4b 100644 --- a/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewRecyclingTester.kt +++ b/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewRecyclingTester.kt @@ -22,7 +22,9 @@ import androidx.compose.runtime.setValue import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.layout.testing.RedwoodLayoutTestingWidgetFactory import app.cash.redwood.lazylayout.testing.RedwoodLazyLayoutTestingWidgetFactory +import app.cash.redwood.protocol.guest.guestRedwoodVersion import app.cash.redwood.protocol.host.ProtocolBridge +import app.cash.redwood.protocol.host.hostRedwoodVersion import app.cash.redwood.widget.MutableListChildren import app.cash.redwood.widget.Widget import assertk.assertThat @@ -45,7 +47,9 @@ import kotlinx.coroutines.coroutineScope class ViewRecyclingTester( coroutineScope: CoroutineScope, ) { - private val compositionProtocolBridge = TestSchemaProtocolBridge.create() + private val compositionProtocolBridge = TestSchemaProtocolBridge.create( + hostVersion = hostRedwoodVersion, + ) internal val composition = TestRedwoodComposition( scope = coroutineScope, @@ -65,6 +69,7 @@ class ViewRecyclingTester( private val widgetContainer = MutableListChildren() private val widgetBridge = ProtocolBridge( + guestVersion = guestRedwoodVersion, container = widgetContainer, factory = widgetProtocolFactory, eventSink = { throw AssertionError() }, diff --git a/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewTreesTest.kt b/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewTreesTest.kt index c95927622f..af5ea2ec8b 100644 --- a/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewTreesTest.kt +++ b/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewTreesTest.kt @@ -31,7 +31,9 @@ import app.cash.redwood.protocol.PropertyChange import app.cash.redwood.protocol.PropertyTag import app.cash.redwood.protocol.WidgetTag import app.cash.redwood.protocol.guest.ProtocolRedwoodComposition +import app.cash.redwood.protocol.guest.guestRedwoodVersion import app.cash.redwood.protocol.host.ProtocolBridge +import app.cash.redwood.protocol.host.hostRedwoodVersion import app.cash.redwood.ui.Cancellable import app.cash.redwood.ui.OnBackPressedCallback import app.cash.redwood.ui.OnBackPressedDispatcher @@ -113,7 +115,9 @@ class ViewTreesTest { lateinit var protocolChanges: List val composition = ProtocolRedwoodComposition( scope = this + BroadcastFrameClock(), - bridge = TestSchemaProtocolBridge.create(), + bridge = TestSchemaProtocolBridge.create( + hostVersion = hostRedwoodVersion, + ), changesSink = { protocolChanges = it }, widgetVersion = UInt.MAX_VALUE, onBackPressedDispatcher = object : OnBackPressedDispatcher { @@ -139,9 +143,12 @@ class ViewTreesTest { ) val protocolNodes = TestSchemaProtocolFactory(widgetSystem) val widgetContainer = MutableListChildren() - val widgetBridge = ProtocolBridge(widgetContainer, protocolNodes) { - throw AssertionError() - } + val widgetBridge = ProtocolBridge( + guestVersion = guestRedwoodVersion, + container = widgetContainer, + factory = protocolNodes, + eventSink = { throw AssertionError() }, + ) widgetBridge.sendChanges(expected) assertThat(widgetContainer.map { it.value }).isEqualTo(snapshot) diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt index 442623a548..af8917a0ef 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt @@ -92,6 +92,7 @@ class ExampleProtocolBridge private constructor( companion object : ProtocolBridge.Factory { override fun create( + hostVersion: RedwoodVersion, json: Json, mismatchHandler: ProtocolMismatchHandler, ): ExampleProtocolBridge { @@ -173,6 +174,7 @@ internal fun generateProtocolBridge( .addFunction( FunSpec.builder("create") .addModifiers(OVERRIDE) + .addParameter("hostVersion", Protocol.RedwoodVersion) .addParameter("json", KotlinxSerialization.Json) .addParameter("mismatchHandler", ProtocolGuest.ProtocolMismatchHandler) .returns(type) diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt index 77724ea158..b8f75cda8a 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt @@ -38,6 +38,7 @@ internal object Protocol { val ModifierTag = ClassName("app.cash.redwood.protocol", "ModifierTag") val PropertyChange = ClassName("app.cash.redwood.protocol", "PropertyChange") val PropertyTag = ClassName("app.cash.redwood.protocol", "PropertyTag") + val RedwoodVersion = ClassName("app.cash.redwood.protocol", "RedwoodVersion") val WidgetTag = ClassName("app.cash.redwood.protocol", "WidgetTag") } diff --git a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/StandardAppLifecycle.kt b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/StandardAppLifecycle.kt index 33a6cd7d85..c96e9ad591 100644 --- a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/StandardAppLifecycle.kt +++ b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/StandardAppLifecycle.kt @@ -19,10 +19,12 @@ import androidx.compose.runtime.BroadcastFrameClock import androidx.compose.runtime.MonotonicFrameClock import app.cash.redwood.protocol.EventTag import app.cash.redwood.protocol.Id +import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.protocol.WidgetTag import app.cash.redwood.protocol.guest.ProtocolBridge import app.cash.redwood.protocol.guest.ProtocolMismatchHandler import app.cash.redwood.treehouse.AppLifecycle.Host +import app.cash.zipline.ZiplineApiMismatchException import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -36,6 +38,14 @@ public class StandardAppLifecycle( private var started = false private lateinit var host: Host + internal val hostProtocolVersion: RedwoodVersion get() { + return try { + host.hostProtocolVersion + } catch (_: ZiplineApiMismatchException) { + RedwoodVersion.Unknown + } + } + private val broadcastFrameClock: BroadcastFrameClock = BroadcastFrameClock { if (started) { host.requestFrame() diff --git a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt index 14ed7c4e6c..ad7a63a3e9 100644 --- a/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt +++ b/redwood-treehouse-guest/src/commonMain/kotlin/app/cash/redwood/treehouse/treehouseCompose.kt @@ -41,8 +41,11 @@ import kotlinx.coroutines.plus public fun TreehouseUi.asZiplineTreehouseUi( appLifecycle: StandardAppLifecycle, ): ZiplineTreehouseUi { - val bridge = - appLifecycle.protocolBridgeFactory.create(appLifecycle.json, appLifecycle.mismatchHandler) + val bridge = appLifecycle.protocolBridgeFactory.create( + hostVersion = appLifecycle.hostProtocolVersion, + json = appLifecycle.json, + mismatchHandler = appLifecycle.mismatchHandler, + ) return RedwoodZiplineTreehouseUi(appLifecycle, this, bridge) } diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/ChangeListRenderer.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/ChangeListRenderer.kt index 27f5d98f14..bb6a48a6ad 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/ChangeListRenderer.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/ChangeListRenderer.kt @@ -19,6 +19,7 @@ import app.cash.redwood.protocol.EventSink import app.cash.redwood.protocol.SnapshotChangeList import app.cash.redwood.protocol.host.ProtocolBridge import app.cash.redwood.protocol.host.ProtocolMismatchHandler +import app.cash.redwood.protocol.host.hostRedwoodVersion import kotlinx.serialization.json.Json /** @@ -40,6 +41,8 @@ public class ChangeListRenderer( ) { view.reset() val bridge = ProtocolBridge( + // Use latest host version as the guest version to avoid any compatibility behavior. + guestVersion = hostRedwoodVersion, container = view.children, factory = view.widgetSystem.widgetFactory( json, diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/RealAppLifecycleHost.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/RealAppLifecycleHost.kt index cd6d86b9ca..81d89ab6c5 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/RealAppLifecycleHost.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/RealAppLifecycleHost.kt @@ -18,6 +18,7 @@ package app.cash.redwood.treehouse import app.cash.redwood.protocol.EventTag import app.cash.redwood.protocol.Id import app.cash.redwood.protocol.WidgetTag +import app.cash.redwood.protocol.host.hostRedwoodVersion internal class RealAppLifecycleHost( private val appLifecycle: AppLifecycle, @@ -25,6 +26,8 @@ internal class RealAppLifecycleHost( private val eventPublisher: EventPublisher, private val codeSession: CodeSession<*>, ) : AppLifecycle.Host { + override val hostProtocolVersion get() = hostRedwoodVersion + override fun requestFrame() { frameClock.requestFrame(appLifecycle) } diff --git a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseAppContent.kt b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseAppContent.kt index d61c023a2f..e6c7ec0d5f 100644 --- a/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseAppContent.kt +++ b/redwood-treehouse-host/src/commonMain/kotlin/app/cash/redwood/treehouse/TreehouseAppContent.kt @@ -18,6 +18,7 @@ package app.cash.redwood.treehouse import app.cash.redwood.protocol.Change import app.cash.redwood.protocol.Event import app.cash.redwood.protocol.EventSink +import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.protocol.host.ProtocolBridge import app.cash.redwood.protocol.host.ProtocolFactory import app.cash.redwood.ui.OnBackPressedCallback @@ -352,6 +353,8 @@ private class ViewContentCodeBinding( @Suppress("UNCHECKED_CAST") // We don't have a type parameter for the widget type. bridgeOrNull = ProtocolBridge( + // TODO Wire through guest version. Wanted this from AppLifecycle but it's bound too late. + guestVersion = RedwoodVersion.Unknown, container = view.children as Widget.Children, factory = view.widgetSystem.widgetFactory( json = codeSession.json, diff --git a/redwood-treehouse/api/android/redwood-treehouse.api b/redwood-treehouse/api/android/redwood-treehouse.api index ddf47b3612..489902bc52 100644 --- a/redwood-treehouse/api/android/redwood-treehouse.api +++ b/redwood-treehouse/api/android/redwood-treehouse.api @@ -23,6 +23,7 @@ public final class app/cash/redwood/treehouse/AppLifecycle$DefaultImpls { public abstract interface class app/cash/redwood/treehouse/AppLifecycle$Host : app/cash/zipline/ZiplineService { public static final field Companion Lapp/cash/redwood/treehouse/AppLifecycle$Host$Companion; + public abstract fun getHostProtocolVersion-7jYel6c ()Ljava/lang/String; public abstract fun handleUncaughtException (Ljava/lang/Throwable;)V public abstract fun onUnknownEvent-_LM6m-c (II)V public abstract fun onUnknownEventNode-1ccMwuE (II)V diff --git a/redwood-treehouse/api/jvm/redwood-treehouse.api b/redwood-treehouse/api/jvm/redwood-treehouse.api index ddf47b3612..489902bc52 100644 --- a/redwood-treehouse/api/jvm/redwood-treehouse.api +++ b/redwood-treehouse/api/jvm/redwood-treehouse.api @@ -23,6 +23,7 @@ public final class app/cash/redwood/treehouse/AppLifecycle$DefaultImpls { public abstract interface class app/cash/redwood/treehouse/AppLifecycle$Host : app/cash/zipline/ZiplineService { public static final field Companion Lapp/cash/redwood/treehouse/AppLifecycle$Host$Companion; + public abstract fun getHostProtocolVersion-7jYel6c ()Ljava/lang/String; public abstract fun handleUncaughtException (Ljava/lang/Throwable;)V public abstract fun onUnknownEvent-_LM6m-c (II)V public abstract fun onUnknownEventNode-1ccMwuE (II)V diff --git a/redwood-treehouse/api/zipline-api.toml b/redwood-treehouse/api/zipline-api.toml index f7a206b5aa..c3fc110e19 100644 --- a/redwood-treehouse/api/zipline-api.toml +++ b/redwood-treehouse/api/zipline-api.toml @@ -28,6 +28,9 @@ functions = [ # fun requestFrame(): kotlin.Unit "/TkBiP/u", + + # val hostProtocolVersion: app.cash.redwood.protocol.RedwoodVersion + "4ifoT9Ua", ] [app.cash.redwood.treehouse.AppService] diff --git a/redwood-treehouse/src/commonMain/kotlin/app/cash/redwood/treehouse/AppLifecycle.kt b/redwood-treehouse/src/commonMain/kotlin/app/cash/redwood/treehouse/AppLifecycle.kt index 87810a9357..88797cb19c 100644 --- a/redwood-treehouse/src/commonMain/kotlin/app/cash/redwood/treehouse/AppLifecycle.kt +++ b/redwood-treehouse/src/commonMain/kotlin/app/cash/redwood/treehouse/AppLifecycle.kt @@ -17,6 +17,7 @@ package app.cash.redwood.treehouse import app.cash.redwood.protocol.EventTag import app.cash.redwood.protocol.Id +import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.protocol.WidgetTag import app.cash.zipline.ZiplineService import kotlin.native.ObjCName @@ -30,6 +31,13 @@ public interface AppLifecycle : ZiplineService { /** Platform features to the guest application. */ public interface Host : ZiplineService { + /** + * The Redwood version of the host. + * This may be used to alter the behavior to work around bugs discovered in the future, and to + * ensure the serialized protocol is remains compatible with what the host expects. + */ + public val hostProtocolVersion: RedwoodVersion + public fun requestFrame() /** Notify the host that an event was unrecognized and will be ignored. */