Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions compose/api/compose.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ final class app.tinyui.schema/ComponentSchema { // app.tinyui.schema/ComponentSc
}

final class app.tinyui/BuildManifest { // app.tinyui/BuildManifest|null[0]
constructor <init>(kotlin.collections/List<kotlin/String>, kotlin.collections/List<kotlin/String>, kotlin.collections/Map<kotlin/String, kotlin/String>, kotlin.collections/Map<kotlin/String, kotlin/String>, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/Int, kotlin.collections/Map<kotlin/String, kotlin/String>, kotlin/String = ..., kotlin.collections/Map<kotlin/String, app.tinyui/PageRequires> = ..., kotlin/String? = ...) // app.tinyui/BuildManifest.<init>|<init>(kotlin.collections.List<kotlin.String>;kotlin.collections.List<kotlin.String>;kotlin.collections.Map<kotlin.String,kotlin.String>;kotlin.collections.Map<kotlin.String,kotlin.String>;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.Int;kotlin.collections.Map<kotlin.String,kotlin.String>;kotlin.String;kotlin.collections.Map<kotlin.String,app.tinyui.PageRequires>;kotlin.String?){}[0]
constructor <init>(kotlin.collections/List<kotlin/String>, kotlin.collections/List<kotlin/String>, kotlin.collections/Map<kotlin/String, kotlin/String>, kotlin.collections/Map<kotlin/String, kotlin/String>, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin.collections/Map<kotlin/String, kotlin/String>, kotlin/String = ..., kotlin.collections/Map<kotlin/String, app.tinyui/PageRequires> = ..., kotlin/String? = ...) // app.tinyui/BuildManifest.<init>|<init>(kotlin.collections.List<kotlin.String>;kotlin.collections.List<kotlin.String>;kotlin.collections.Map<kotlin.String,kotlin.String>;kotlin.collections.Map<kotlin.String,kotlin.String>;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.collections.Map<kotlin.String,kotlin.String>;kotlin.String;kotlin.collections.Map<kotlin.String,app.tinyui.PageRequires>;kotlin.String?){}[0]

final val buildIds // app.tinyui/BuildManifest.buildIds|{}buildIds[0]
final fun <get-buildIds>(): kotlin.collections/Map<kotlin/String, kotlin/String> // app.tinyui/BuildManifest.buildIds.<get-buildIds>|<get-buildIds>(){}[0]
Expand All @@ -266,8 +266,6 @@ final class app.tinyui/BuildManifest { // app.tinyui/BuildManifest|null[0]
final fun <get-name>(): kotlin/String // app.tinyui/BuildManifest.name.<get-name>|<get-name>(){}[0]
final val pages // app.tinyui/BuildManifest.pages|{}pages[0]
final fun <get-pages>(): kotlin.collections/List<kotlin/String> // app.tinyui/BuildManifest.pages.<get-pages>|<get-pages>(){}[0]
final val protocol // app.tinyui/BuildManifest.protocol|{}protocol[0]
final fun <get-protocol>(): kotlin/Int // app.tinyui/BuildManifest.protocol.<get-protocol>|<get-protocol>(){}[0]
final val publicKey // app.tinyui/BuildManifest.publicKey|{}publicKey[0]
final fun <get-publicKey>(): kotlin/String // app.tinyui/BuildManifest.publicKey.<get-publicKey>|<get-publicKey>(){}[0]
final val requires // app.tinyui/BuildManifest.requires|{}requires[0]
Expand Down Expand Up @@ -459,8 +457,6 @@ final class app.tinyui/PageHost { // app.tinyui/PageHost|null[0]
final object Companion { // app.tinyui/PageHost.Companion|null[0]
final const val MODULE_SCHEME // app.tinyui/PageHost.Companion.MODULE_SCHEME|{}MODULE_SCHEME[0]
final fun <get-MODULE_SCHEME>(): kotlin/String // app.tinyui/PageHost.Companion.MODULE_SCHEME.<get-MODULE_SCHEME>|<get-MODULE_SCHEME>(){}[0]
final const val PROTOCOL // app.tinyui/PageHost.Companion.PROTOCOL|{}PROTOCOL[0]
final fun <get-PROTOCOL>(): kotlin/Int // app.tinyui/PageHost.Companion.PROTOCOL.<get-PROTOCOL>|<get-PROTOCOL>(){}[0]
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class BundleSmokeTest {
for (fn in listOf("__host_apply", "__host_query", "__host_call", "__host_send", "__host_report")) engine.registerFunction(fn) { JsValue.Undefined }
assertEquals("tinyui-core", engine.registerModule(out.resolve("runtime/core.bin").readBytes()))
assertEquals("tinyui-native", engine.registerModule(out.resolve("runtime/native.bin").readBytes()))
engine.evaluateModule("import \"tinyui-core\"; export const p = globalThis.__tinyui.protocol;").use {
assertEquals(JsValue.Num(PageHost.PROTOCOL), it.get("p"))
engine.evaluateModule("import \"tinyui-core\"; export const v = globalThis.__tinyui.version;").use {
assertEquals(JsValue.Str(TinyUI.version), it.get("v"))
}
engine.runBytecode(out.resolve("pages/counter.bin").readBytes()).let { if (it is AutoCloseable) it.close() }
}
Expand Down
3 changes: 0 additions & 3 deletions compose/src/commonMain/kotlin/app/tinyui/BuildManifest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package app.tinyui

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
Expand All @@ -18,7 +17,6 @@ class BuildManifest(
val version: String,
val createdAt: String,
val engine: String,
val protocol: Int,
val hashes: Map<String, String>,
/** The tinyui-core version the runtime modules came from. */
val tinyui: String = "",
Expand Down Expand Up @@ -53,7 +51,6 @@ class BuildManifest(
version = string("version"),
createdAt = string("createdAt"),
engine = string("engine"),
protocol = root["protocol"]?.jsonPrimitive?.intOrNull ?: 0,
hashes = strings("hashes"),
tinyui = string("tinyui"),
requires = root["requires"]?.jsonObject?.mapValues { (_, v) ->
Expand Down
1 change: 0 additions & 1 deletion compose/src/commonMain/kotlin/app/tinyui/PackageCheck.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ object PackageCheck {
/** Every reason [manifest]'s package cannot run on [host]; empty when it can. */
fun problems(manifest: BuildManifest, host: TinyUIHost): List<String> = buildList {
if (manifest.engine != QuickJs.upstreamCommit) add("built for engine ${manifest.engine}, the host embeds ${QuickJs.upstreamCommit}")
if (manifest.protocol != PageHost.PROTOCOL) add("built for protocol ${manifest.protocol}, the host implements ${PageHost.PROTOCOL}")
if (manifest.tinyui != TinyUI.version) add("built with tinyui ${manifest.tinyui.ifEmpty { "(unknown)" }}, the host has ${TinyUI.version}")
val capabilities = FrameworkCapabilities.toSet() + host.capabilities.names
for ((page, uses) in manifest.requires.entries.sortedBy { it.key }) {
Expand Down
6 changes: 2 additions & 4 deletions compose/src/commonMain/kotlin/app/tinyui/PageHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ class PageHost(
val namespace = runBytecode(page.bytecode, ObjectTransport.REF) as JsRef
if (namespace.isPromise) { namespace.close(); error("page module is still pending after microtasks were drained") }
val self = evaluate("__tinyui", objects = ObjectTransport.REF) as JsRef
val version = (self.get("version") as? JsValue.Str)?.value
check(version == TinyUI.version) { "runtime module is tinyui $version, the host has ${TinyUI.version}" }
val fns = ENTRY_NAMES.associateWith { self.get(it, ObjectTransport.REF) as JsRef }
val e = Entries(self, fns).also { entries = it }
val protocol = (self.get("protocol") as? JsValue.Num)?.value?.toInt()
check(protocol == PROTOCOL) { "protocol $protocol from the runtime module, host implements $PROTOCOL" }
namespace.use { ns ->
(ns.get("default", ObjectTransport.REF) as JsRef).use { default ->
e.call("mount", default, JsValue.Str(propsJson), JsValue.Str(registry.manifest(FrameworkCapabilities + host.capabilities.names)))
Expand Down Expand Up @@ -373,8 +373,6 @@ class PageHost(
companion object {
/** `import.meta.url` of a page module is `tinyui:<name>` (docs/adr-005-engine.md). */
const val MODULE_SCHEME = "tinyui"
/** docs/patch-protocol.md §6 */
const val PROTOCOL = 1
private val ENTRY_NAMES = listOf("mount", "unmount", "visible", "dispatch", "resolve", "reject", "emit", "flush")
}
}
4 changes: 2 additions & 2 deletions compose/src/commonTest/kotlin/app/tinyui/BundleTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class BundleTest {
"version": "20260922T090000Z-3f2a1c",
"createdAt": "2026-09-22T09:00:00Z",
"engine": "04be246001599f5995fa2f2d8c91a0f198d3f34c",
"protocol": 1,
"tinyui": "0.5.0",
"hashes": { "tinyui-core": "00", "tinyui-native": "01", "shop/home": "02", "shop/cart": "03" }
}
""".trimIndent()
Expand All @@ -50,7 +50,7 @@ class BundleTest {
val bundle = Bundle.load(files())
assertEquals("shop", bundle.name)
assertEquals("20260922T090000Z-3f2a1c", bundle.manifest.version)
assertEquals(1, bundle.manifest.protocol)
assertEquals("0.5.0", bundle.manifest.tinyui)
assertNull(bundle.manifest.hostVersion, "an embedded manifest has no hostVersion")
val page = bundle.page("shop/home")
assertEquals("shop/home", page.module.name)
Expand Down
5 changes: 1 addition & 4 deletions compose/src/commonTest/kotlin/app/tinyui/PackageCheckTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ class PackageCheckTest {

private fun manifest(
engine: String = QuickJs.upstreamCommit,
protocol: Int = PageHost.PROTOCOL,
tinyui: String = TinyUI.version,
requires: Map<String, PageRequires> = mapOf("shop/home" to PageRequires(listOf("ta.Icon"), listOf("checkout.start", "store.get"))),
) = BuildManifest(
runtime = emptyList(), pages = listOf("shop/home"), files = emptyMap(), buildIds = emptyMap(),
name = "shop", publicKey = "k", version = "v1", createdAt = "2026-09-24T00:00:00Z",
engine = engine, protocol = protocol, hashes = emptyMap(), tinyui = tinyui, requires = requires,
engine = engine, hashes = emptyMap(), tinyui = tinyui, requires = requires,
)

@Test
Expand All @@ -42,7 +41,6 @@ class PackageCheckTest {
val problems = PackageCheck.problems(
manifest(
engine = "0".repeat(40),
protocol = 99,
tinyui = "0.0.1",
requires = mapOf("shop/home" to PageRequires(listOf("ta.Rating"), listOf("coupon.apply"))),
),
Expand All @@ -51,7 +49,6 @@ class PackageCheckTest {
assertEquals(
listOf(
"built for engine ${"0".repeat(40)}, the host embeds ${QuickJs.upstreamCommit}",
"built for protocol 99, the host implements ${PageHost.PROTOCOL}",
"built with tinyui 0.0.1, the host has ${TinyUI.version}",
"shop/home uses component ta.Rating, which the host does not register",
"shop/home calls coupon.apply, which the host does not provide",
Expand Down
7 changes: 4 additions & 3 deletions compose/src/commonTest/kotlin/app/tinyui/PageHostTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class PageHostTest {
private val core = module("tinyui-core", """
let patches = [], count = 0, handler = null, page = null, emits = 0, settled = []; const settle = (line) => { settled.push(line); patches.push(["p",1,"text", settled.slice().sort().join(";")]); };
globalThis.__tinyui = {
protocol: 1,
version: "$VERSION",
mount(p, props, host) {
page = p; const name = JSON.parse(props).name ?? "world";
patches.push(["c",1,"Text"],["p",1,"text","hello " + name + " / " + Object.keys(JSON.parse(host).components).length],
Expand Down Expand Up @@ -210,12 +210,13 @@ class PageHostTest {
}

@Test
fun protocolMismatchIsE6() = runTest {
val badCore = module("tinyui-core", "globalThis.__tinyui = { protocol: 99 }; export const VERSION = 'x';")
fun runtimeFromAnotherTinyuiIsE6() = runTest {
val badCore = module("tinyui-core", "globalThis.__tinyui = { version: '0.0.1' }; export const VERSION = '0.0.1';")
val host = PageHost(RuntimeBundle(badCore, native), page, tinyui)
host.start()
host.await { host.failure != null }
assertEquals("E6", host.failure!!.kind)
assertEquals("runtime module is tinyui 0.0.1, the host has ${TinyUI.version}", host.failure!!.message)
assertEquals("E6", errors.single().kind)
host.close()
}
Expand Down
8 changes: 4 additions & 4 deletions docs/adr-006-hot-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ ADR-005 把热下发划出当期,条件是"内核稳定后另立 ADR"。Trendi
|---|---|---|
| 页面 = 模块 = 字节码,自包含无共享 chunk | build-chain.md §3 | 包只是页面的集合,分发单元不用再设计 |
| 字节码与字长无关、只绑引擎 commit,文件头自带 commit,加载时自校验 | quickjs-kmp `docs/decisions.md` | Android / iOS 共用同一份包;引擎不匹配是确定性的 E6 |
| `PROTOCOL` 在 K0 核对 → E6;schema 不匹配走 E5 跳过 + `Placeholder`;`__mount` 清单让 JS `host.has()` 降级 | patch-protocol.md §6–7、ADR-003 | 硬兼容 / 软兼容的分界已画好,热下发只需把硬的两条提前到下载前 |
| tinyui 版本在 K0 核对 → E6;schema 不匹配走 E5 跳过 + `Placeholder`;`__mount` 清单让 JS `host.has()` 降级 | patch-protocol.md §6–7、ADR-003 | 硬兼容 / 软兼容的分界已画好,热下发只需把硬的两条提前到下载前 |
| `PageHost` 只收字节,库零 I/O,读资源在宿主 | `PageHost.kt`、TrendingAI `TinyUIHost.kt` | 热下发对库来说是"多一个字节来源 + 选哪个" |
| release 不带 map,靠 `buildId` 离线对映射 | build-chain.md §7 | 错误上报链路不变 |

Expand All @@ -42,7 +42,7 @@ ADR-005 把热下发划出当期,条件是"内核稳定后另立 ADR"。Trendi
| 谁负责判断 | 人(发版时) | 服务端逻辑 | 客户端解析 |
| 静态托管可行 | 是(作路径) | 否 | 勉强 |

选 hostVersion,它同时是服务端目录名。`engine`(引擎 commit)与 `protocol` 写进 manifest 只做校验,防"发错目录"这种人为错误,不参与匹配逻辑。
选 hostVersion,它同时是服务端目录名。`engine`(引擎 commit)与 `tinyui`(版本)写进 manifest 只做校验,防"发错目录"这种人为错误,不参与匹配逻辑。

兼容键回答的是"这一版页面能发给哪些已装好的 App"。页面依赖宿主给它的东西——宿主组件、宿主能力、tinyui 本身——而宿主只能随 App 发版改,页面随时可以热下发,两边的新旧必然错开。以一个订阅页为例:

Expand Down Expand Up @@ -154,7 +154,7 @@ ADR-005 把热下发划出当期,条件是"内核稳定后另立 ADR"。Trendi
|---|---|
| 包 | App 由 N≥1 个包组成;一个包 = 一次 `tinyui build` 的完整产物(`runtime/*.bin` + `pages/**/*.bin` + `manifest.json`),整包原子生效,各包独立;不做单页下发、不做跨包共享模块、不做 zip、不做 diff |
| 包名与模块名 | 包名 `[a-z0-9-]+` 来自 `tinyui.config.json`;模块名 = 路由键 = `<pkg>/<相对路径>`,无 `pages/` 段 |
| 兼容键 | 宿主声明的 `hostVersion`,作服务端路径;`engine` / `protocol` 只校验 |
| 兼容键 | 宿主声明的 `hostVersion`,作服务端路径;`engine` / `tinyui` 只校验 |
| 启用规则 | 下发包 `version ≠ installed` 且 `createdAt` 新于内置才装;App 升级带来更新的内置包时自动弃掉 installed;不能用下发把 App 降到比内置更老 |
| 生效时机 | 下次进程启动 |
| 失败语义 | 下发包的页面报 E2 / E6 → 当场用内置包重挂这一页,整包持久化拉黑并上报;E5 不算失败。启动时重算 installed 包 sha256,不一致同样拉黑。进程级崩溃不在库的回退范围:出口是灰度 + 回滚 + "`check()` 先于任何页面挂载"的接入约定,挂载标记见 §4.3。一个 (runtime, page) 对永远来自同一个包;不同页面可以短暂来自不同包(各自独立 Runtime) |
Expand All @@ -163,7 +163,7 @@ ADR-005 把热下发划出当期,条件是"内核稳定后另立 ADR"。Trendi
| 灰度 | manifest `rollout` 百分比,客户端按 `installId` 掷骰 |
| 回滚 | 用旧内容发新版本,指针只往前走;无 `rollBackToEmbedded` |
| 完整性 | 逐文件 sha256 + manifest 签名(ECDSA P-256,覆盖 `<version>/manifest.json` 的原始字节,无规范化);密钥按 (app, pkg),发布方持私钥,服务端与客户端各验一次;公钥写在 `tinyui.config.json` 随 build 进 manifest,客户端只信内置包的 |
| CLI | `tinyui.config.json`(`name` / `publicKey` / `pages`);`tinyui build` 的 manifest 加 `name` / `publicKey` / `version` / `createdAt` / `engine` / `protocol` / `hashes`;`tinyui bundle --host-version --signing-key` 产出上传目录 `dist/ota/<pkg>/<hostVersion>/`(`current.json` + `<version>/`);`tinyui publish` 走发布协议;`keys` / `apps` / `packages` / `tokens` / `releases` 子命令是全部管理面 |
| CLI | `tinyui.config.json`(`name` / `publicKey` / `pages`);`tinyui build` 的 manifest 加 `name` / `publicKey` / `version` / `createdAt` / `engine` / `tinyui` / `hashes`;`tinyui bundle --host-version --signing-key` 产出上传目录 `dist/ota/<pkg>/<hostVersion>/`(`current.json` + `<version>/`);`tinyui publish` 走发布协议;`keys` / `apps` / `packages` / `tokens` / `releases` 子命令是全部管理面 |
| 服务端 | 独立开源仓 `tinyui-updates-server`(MIT,托管前重评):Hono,单 R2 桶,`Storage` 接口后置;内容按 (app, pkg, hostVersion, version) 存,channel 是指针;托管实例 `updates.tinyui.app`,私有化 = 部署同一份代码;没有控制台 |
| 仓边界 | `tinyui-updates` 在主仓 `updates/` 模块,与 core 同版本发;服务端独立仓;协议只写在 updates.md,发布后字段只增不改 |

Expand Down
Loading
Loading