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
14 changes: 10 additions & 4 deletions Agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@
第一次回到仓库时,优先按这个顺序走:

1. 先看 `package.json` 里的脚本,确认日常入口还是 `yarn dev`、`yarn build`、`yarn test:koka`。
2. 再看 `app.kk`,确认当前浏览器桥只暴露哪些 Koka 入口。
3. 然后看 `explore/react/*` 和 `demo/*` 的边界:前者是库,后者是 demo 和业务。
4. 开始改代码前,先跑一次 `yarn build`,确认自己不是站在坏状态上继续开发。
2. 组件作者先看 `docs/quick-start.md` 与 `docs/component-authoring.md`;不要从 runtime snapshot 实现反推日常 API。
3. 再看 `app.kk`,确认当前浏览器桥只暴露哪些 Koka 入口。
4. 然后看 `explore/react/*` 和 `demo/*` 的边界:前者是库,后者是 demo 和业务。
5. 开始改代码前,先跑一次 `yarn build`,确认自己不是站在坏状态上继续开发。

## 仓库结构

- 仓库根目录:就是 Koka 源码根目录,编译时直接把 repo root 当成模块搜索根。
- `app.kk`:浏览器入口,只暴露 boot、事件桥接和 runtime snapshot 导入导出。
- `explore/react/*`:核心 VDOM、typed component store、listener registry、render、diff/patch。这里尽量保持通用。
- `explore/react/core.kk`、`action.kk`、`state.kk`:component authoring surface;业务 view 只从这里获取 elements、actions、stores、keyed lifecycle 与 effects。
- `explore/react/runtime.kk`:browser/app host 使用的 registered callback、scheduled effect 与 snapshot transport。
- `explore/react/inspection.kk`:tests/devtools 使用的 VDOM、event registry 与 runtime entry 只读查询。
- `explore/react/renderer.kk`:host/tests 使用的 render、diff 与 patch;不要导入业务 view。
- `demo/*`:具体 demo、布局、组件、路由和测试辅助。
- `demo/runtimeframe.kk`:app 边界的 runtime owner,同时持有业务 model 和框架 state tree。
- `runtime/*`:只放 DOM 和系统边界的 FFI,不要把业务逻辑塞进来。
Expand Down Expand Up @@ -148,6 +152,8 @@ chrome-devtools take_screenshot --fullPage --filePath .tmp-devtools-full.png

组件交互状态以 **typed store + serializable actions** 为默认方案。它保留 React reducer 的简单心智模型,同时满足 Koka 严格类型、HMR 和 snapshot 恢复需求。

- 普通业务 view 只导入 `explore/react/core`、`explore/react/action`、`explore/react/state` 中实际需要的模块;不得导入 `explore/react/runtime`、`explore/react/inspection` 或 `explore/react/renderer`。

- 业务组件用 `(state, dispatch) = use_store(spec, initial = ...)` 读取 reducer pair,不额外暴露 binding record。
- UI 事件优先用 `on_store_click(name, action = ..., dispatch = ...)` / `on_store_input(...)` 发送 typed action,不直接操作 state tree。
- domain action 事件优先用 `on_action_click(name, action = ..., dispatch = ...)` / `on_action_input(...)` / `on_action_enter(...)`,不要在每个 element 内重复 forwarding closure。
Expand Down
10 changes: 5 additions & 5 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ feature_dom_marker(group = ..., key = ..., name = ...)

### 3. 拆分 component facade 与 runtime/testing API

- 将普通组件需要的 `component/components/use_store/state_effect/on_*` 收敛到 facade;
- 将 runner、registry、snapshot 与 tree inspection 移到 runtime/testing 模块;
- opaque feature identity 已进入 `feature_root`;下一步把显式 key/path inspection API 物理拆到 runtime/testing module;
- 普通组件的 authoring surface 已明确收敛在 `core/action/state`,不额外增加只做转发的 facade;
- registered callback runner、scheduled effect、snapshot transport 已物理移到 `explore/react/runtime`;VDOM、event registry 与 entry count 查询已物理移到 `explore/react/inspection`;
- opaque feature identity 已进入 `feature_root`;显式 key/path 与 store inspection 目前仍和 state tree 实现共享私有依赖,只有能移动真实实现且不产生单行 forwarding wrapper 时再继续拆分;
- 为 controlled component 固定“主 domain value 位置参数 + labelled callback/config props”的签名模板,避免每个 feature 再造 props adapter;
- scope 计算只保留在确有跨组件协调的 state 模块;
- reducer、codec、store spec 尽量同模块定义,view 只 import typed surface;
Expand Down Expand Up @@ -200,8 +200,8 @@ issue、PR 及影响结论的进度更新统一使用中英双语:标题采用
- [#7 Hide feature identity and remove cross-component store path coordination](https://github.com/Respo/explore-react.koka/issues/7):已由 PR #10 合并;
- [#8 减少 typed store 样板代码并显式选择 replay 恢复 / Reduce typed store boilerplate with explicit replay recovery](https://github.com/Respo/explore-react.koka/issues/8):已由 PR #14 合并;snapshot/replay recovery 选择与 Todo editor session 语义已落地;
- [#9 Define lifecycle cleanup for unreachable child component stores](https://github.com/Respo/explore-react.koka/issues/9):已由 PR #11 合并;
- [#12 简化组件事件中的 domain 与 local-store transition / Simplify domain and local-store transitions in component events](https://github.com/Respo/explore-react.koka/issues/12):当前实现批次;以四个真实调用点、action 顺序测试和组件定义可读性作为是否保留公共 API 的标准;
- [#13 发布渐进式组件作者 API / Publish a progressive-disclosure component authoring surface](https://github.com/Respo/explore-react.koka/issues/13):等待 #12 的 transition API 完成使用者评估后,整理 quick start、authoring API 与 module 边界。
- [#12 简化组件事件中的 domain 与 local-store transition / Simplify domain and local-store transitions in component events](https://github.com/Respo/explore-react.koka/issues/12):已由 PR #15 合并;四个真实调用点共享 event-independent transition,并覆盖 action 顺序与浏览器回归;
- [#13 发布渐进式组件作者 API / Publish a progressive-disclosure component authoring surface](https://github.com/Respo/explore-react.koka/issues/13):当前实现批次;以四概念 quick start、单页 author API、advanced module import 边界作为验收标准。

## 验证标准

Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ This repo explores a React-like component runtime in Koka, with Algebraic
Effects used for browser capabilities, test substitution, component-local
state, and event dispatch.

## Start with component authoring

Learn the normal path as four tasks; runtime ownership and snapshot transport
are advanced integration topics, not prerequisites:

1. **Elements:** write ordinary view functions with positional content and
labelled attributes/events.
2. **Keyed components:** add `component(...)` / `components(...)` only when a
child needs stable lifecycle identity.
3. **Typed stores/actions:** define a serializable action and use
`(state, dispatch) = use_store(spec, initial = ...)`.
4. **Effects:** declare post-render work with `state_effect(...)`, and keep
browser/service capabilities as explicit Koka effects.

Start with the bilingual [component quick start](docs/quick-start.md), then use
the [one-page component author API](docs/component-authoring.md) as the normal
reference. Persistent feature identity, recovery, and cross-domain/local
transitions are introduced only when needed:

- [component lifecycle](docs/component-lifecycle.md)
- [store recovery](docs/store-recovery.md)
- [action/store transitions](docs/action-store-transitions.md)

The current design deliberately separates two kinds of state:

- domain state lives in the application `model` and changes through typed
Expand Down Expand Up @@ -472,8 +495,14 @@ diffing, and patch planning remain in Koka.
- `explore/react/action.kk`: serializable action codecs, envelopes, and the
observation effect.
- `explore/react/state.kk`: component scopes, typed stores, listeners, effects,
snapshots, and runtime handlers.
and lifecycle authoring.
- `explore/react/runtime.kk`: host-only registered callback execution,
scheduled effects, and snapshot transport.
- `explore/react/inspection.kk`: read-only VDOM/event-registry/runtime queries
for tests and devtools.
- `explore/react/renderer.kk`: rendering, diffing, and patch planning.
- `docs/quick-start.md` and `docs/component-authoring.md`: progressive
component tutorial and the compact preferred API.
- `demo/*`: application shell, features, actions, stores, and workflows.
- `demo/runtimeframe.kk`: pairs the domain model with the framework runtime tree
and runs render/action transitions at the app boundary.
Expand Down
2 changes: 2 additions & 0 deletions boilerplate/browserapp.kk
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import library/bridge
import library/route
import explore/react/action
import explore/react/state
import explore/react/runtime
import explore/react/inspection

noinline val root_id_ref : ref<global,string> = unsafe-total { ref("app") }
noinline val frame_ref : ref<global,runtime_frame> = unsafe-total { ref(current_runtime_frame(initial_model(), Nil)) }
Expand Down
2 changes: 2 additions & 0 deletions demo/tests/appharness.kk
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import demo/tests/support
import library/bridge
import explore/react/core
import explore/react/state
import explore/react/runtime
import explore/react/inspection

fun app_harness_render(previous_owner : maybe<model>, frame : runtime_frame) : <div> component_harness
val (next_frame, tree, effects, registry) = run_runtime_render(frame, fn(owner) render_app_with_runtime(previous_owner, owner, Nil))
Expand Down
2 changes: 2 additions & 0 deletions demo/tests/basics.kk
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import explore/react/action
import explore/react/core
import explore/react/renderer
import explore/react/state
import explore/react/runtime
import explore/react/inspection

fun timer_logic() : <browser_host> string
wait_ms(20)
Expand Down
1 change: 1 addition & 0 deletions demo/tests/dialogcases.kk
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import library/dialog
import library/route
import explore/react/core
import explore/react/state
import explore/react/inspection

pub fun dialog_alert_open_test() : <div> test_result
with with_test_browser(False, "T+0")
Expand Down
1 change: 1 addition & 0 deletions demo/tests/labcases.kk
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import demo/tests/support
import explore/react/core
import explore/react/action
import explore/react/state
import explore/react/inspection

pub fun lab_local_routing_test() : <div> test_result
with with_test_browser(False, "T+120")
Expand Down
2 changes: 2 additions & 0 deletions demo/tests/statecases.kk
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import demo/todo/state
import explore/react/action
import explore/react/core
import explore/react/state
import explore/react/runtime
import explore/react/inspection

pub fun state_tree_test() : <div> test_result
val incident_scope = "lab/incidents/101"
Expand Down
2 changes: 2 additions & 0 deletions demo/tests/support.kk
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import library/search
import explore/react/action
import explore/react/core
import explore/react/state
import explore/react/runtime
import explore/react/inspection

pub struct test_result(name : string, detail : string, passed : bool)

Expand Down
2 changes: 2 additions & 0 deletions demo/tests/todocases.kk
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import explore/react/action
import explore/react/core
import explore/react/renderer
import explore/react/state
import explore/react/runtime
import explore/react/inspection

pub fun todo_editor_store_protocol_test() : <div> test_result
val scope_group = "tests"
Expand Down
139 changes: 139 additions & 0 deletions docs/component-authoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Component author API

# 中文

普通业务组件只导入三个模块:

```koka
import explore/react/core
import explore/react/action
import explore/react/state
```

这三个模块分别承载 elements/VDOM、serializable action protocol,以及 component lifecycle/store/effect authoring。不要在业务 view 中导入 `explore/react/runtime`、`explore/react/inspection` 或 `explore/react/renderer`。

## 1. Elements

| 任务 | 推荐 API |
| --- | --- |
| container | `div/section/article/aside/header/nav/ul/li(children, class = ..., key = ...)` |
| text element | `span/strong/p/h1/h2/h3(text, class = ..., key = ...)` |
| interaction | `button(text, click = ...)`, `input_text(value, input = ..., enter = ...)` |
| uncommon DOM capability | `extra_attrs` / `extra_events` |

主要内容始终是第一个位置参数;常用属性和事件保持 flat labelled arguments。不要恢复 `attrs = ..., children = ...` 的嵌套形态。

## 2. Keyed components

| 任务 | 推荐 API |
| --- | --- |
| ordinary keyed child | `component(group, key) { ... }` |
| keyed collection | `components(items, group = ..., key = ..., render = ...)` |
| persistent page feature | `feature_root(group, key) { ... }` |
| feature-owned VDOM/effect identity | `feature_key()` / `feature_marker(name)` |

普通 view helper 不建立 identity。只有 local state、effect 或 listener 需要独立 lifecycle 时才建立 keyed boundary。

## 3. Typed stores and actions

| 任务 | 推荐 API |
| --- | --- |
| action protocol | `Action_codec(schema = ..., version = ..., decode = ..., encode = ...)` |
| full-state recovery | `snapshot_store(name = ..., state_codec = ..., action_codec = ..., reduce = ...)` |
| bounded session replay | `replay_store(name = ..., action_codec = ..., replay = ..., reduce = ...)` |
| component reducer pair | `(state, dispatch) = use_store(spec, initial = ...)` |
| component action event | `on_store_click/input(name, action = ..., dispatch = ...)` |
| domain action event | `on_action_click/input/enter(name, action = ..., dispatch = ...)` |
| one domain + one local action | `action_store_transition(action, dispatch = ..., store_action = ..., store = ..., store_when = ...)` |
| custom handler | `on_local_click/input/enter(name, handler)` |

domain reducer 不读取 child store。组件需要提交 draft 时,把值放进 serializable domain action。复杂分支才使用 `on_local_*`。

## 4. Effects

| 任务 | 推荐 API |
| --- | --- |
| render 后按 deps 调度 | `state_effect(name = ..., deps = ..., action = ...)` |
| browser/service capability | Koka `fun` effect + app/test handler |
| ambient read-only value | Koka `val` effect |

effect name 在同一 component boundary 内保持稳定。业务 workflow 显式声明 confirm、timer、request 等 capability;不要隐藏到 snapshot/replay 或 arbitrary callback 中。

## Advanced modules

| 模块 | 使用者 |
| --- | --- |
| `explore/react/runtime` | browser/app host:运行 registered callbacks、scheduled effects、snapshot transport |
| `explore/react/inspection` | tests/devtools:查询 VDOM、event registry 与 runtime entry count |
| `explore/react/renderer` | host/tests:render、diff、patch |

这些模块不是 component authoring surface。更深入的恢复和生命周期规则见 [store recovery](store-recovery.md) 与 [component lifecycle](component-lifecycle.md)。

# English

Ordinary business components import only three modules:

```koka
import explore/react/core
import explore/react/action
import explore/react/state
```

They provide elements/VDOM, the serializable action protocol, and component lifecycle/store/effect authoring. Business views do not import `explore/react/runtime`, `explore/react/inspection`, or `explore/react/renderer`.

## 1. Elements

| Task | Preferred API |
| --- | --- |
| Container | `div/section/article/aside/header/nav/ul/li(children, class = ..., key = ...)` |
| Text element | `span/strong/p/h1/h2/h3(text, class = ..., key = ...)` |
| Interaction | `button(text, click = ...)`, `input_text(value, input = ..., enter = ...)` |
| Uncommon DOM capability | `extra_attrs` / `extra_events` |

Primary content is always the first positional argument. Common attributes and events remain flat labelled arguments; do not restore a nested `attrs = ..., children = ...` shape.

## 2. Keyed components

| Task | Preferred API |
| --- | --- |
| Ordinary keyed child | `component(group, key) { ... }` |
| Keyed collection | `components(items, group = ..., key = ..., render = ...)` |
| Persistent page feature | `feature_root(group, key) { ... }` |
| Feature-owned VDOM/effect identity | `feature_key()` / `feature_marker(name)` |

An ordinary view helper does not establish identity. Add a keyed boundary only when local state, effects, or listeners need an independent lifecycle.

## 3. Typed stores and actions

| Task | Preferred API |
| --- | --- |
| Action protocol | `Action_codec(schema = ..., version = ..., decode = ..., encode = ...)` |
| Full-state recovery | `snapshot_store(name = ..., state_codec = ..., action_codec = ..., reduce = ...)` |
| Bounded session replay | `replay_store(name = ..., action_codec = ..., replay = ..., reduce = ...)` |
| Component reducer pair | `(state, dispatch) = use_store(spec, initial = ...)` |
| Component action event | `on_store_click/input(name, action = ..., dispatch = ...)` |
| Domain action event | `on_action_click/input/enter(name, action = ..., dispatch = ...)` |
| One domain + one local action | `action_store_transition(action, dispatch = ..., store_action = ..., store = ..., store_when = ...)` |
| Custom handler | `on_local_click/input/enter(name, handler)` |

A domain reducer never reads a child store. Put a draft or other current component value into the serializable domain action. Use `on_local_*` only for genuinely custom branching.

## 4. Effects

| Task | Preferred API |
| --- | --- |
| Schedule after render by dependencies | `state_effect(name = ..., deps = ..., action = ...)` |
| Browser/service capability | A Koka `fun` effect with app/test handlers |
| Ambient read-only value | A Koka `val` effect |

Keep effect names stable within one component boundary. Business workflows declare confirm, timer, request, and similar capabilities explicitly; do not hide them in snapshot/replay or arbitrary callbacks.

## Advanced modules

| Module | Audience |
| --- | --- |
| `explore/react/runtime` | Browser/app hosts: registered callbacks, scheduled effects, and snapshot transport |
| `explore/react/inspection` | Tests/devtools: VDOM, event-registry, and runtime-entry queries |
| `explore/react/renderer` | Hosts/tests: render, diff, and patch |

These modules are not part of the component-authoring surface. See [store recovery](store-recovery.md) and [component lifecycle](component-lifecycle.md) for the deeper contracts.
Loading
Loading