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
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
nodeLinker: node-modules
npmMinimalAgeGate: 60
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@

# Respo Guidebook

配对正式 Calcit CLI / `@calcit/procs` 0.30.1,以及已发布的 Docs Workflow 0.0.37、
Respo 0.16.124、Reel 0.6.41、UI 0.8.0 和 JS-FFI 0.2.5。升级后重新安装依赖并完整
刷新页面,不混用旧 CLI/alpha runtime。UI 0.8 基础样式可能影响外观。源码仅使用
配对正式 Calcit CLI / `@calcit/procs` 0.31.0,以及已发布的 Docs Workflow 0.0.38、
Respo 0.17.0-alpha.2、Reel 0.7.0-alpha.2、UI 0.9.0-alpha.2 和 JS-FFI 0.2.5。
工具链为正式版本,模块图仍有预发布版本;不把两者统称为全图稳定。升级后重新安装依赖并完整
刷新页面,不混用旧 CLI/alpha runtime。UI 0.9 基础样式可能影响外观。源码仅使用
`calcit.cirru` 与 `deps.cirru`,不生成 compact/package.cirru。

Guide 说明当前 Ref/watch、Option、widget/app dispatch、名义 Store 和宿主边界。
旧 Beginner Guide/API 仍有历史片段,尚未全部迁移或独立执行;站点构建通过不代表
Guide 说明当前 Ref/watch、Option、event handler/app dispatch、名义 Store 和宿主边界。
旧 Beginner Guide、部分 Guide 实现说明和 API 仍有历史片段,尚未全部迁移或独立执行;站点构建通过不代表
所有旧示例都可用于当前版本。新应用以维护中的模板和已发布接口为准。

## 开发与验收

Node 24、Yarn 4.18.0、正式 0.30.1 CLI:
Node 24、Yarn 4.18.0、正式 0.31.0 CLI:

```bash
corepack enable
Expand All @@ -29,6 +30,10 @@ yarn dev
并保存 PR 预览。原项目没有定义附带测试,零个测试不等于业务覆盖;浏览器导航、
搜索与刷新需另行验收。页面 `hud!` 错误展示保留。

默认 entry 的 dispatch-op 显式绑定原有 renderer 的 Dynamic 宿主入口;
非 Enum 输入仍被拒绝,业务 payload 仍受检后构造 Docs Workflow 的具名 Op。
Store/Op、全部定义 schema 与处理函数未改,不以关闭证明审计或增加断言绕过 strict。

## 部署边界

生产 SPA 引用 `https://cos-sh.tiye.me/Respo/guidebook/` 下的静态 assets,COS 只用
Expand Down
2 changes: 1 addition & 1 deletion calcit.cirru
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{} (:description |) (:init-fn 'app.main/main!) (:mode :js) (:reload-fn 'app.main/reload!) (:target :browser)
:feature-policy $ {}
:modules $ [] |respo.calcit/ |respo-ui.calcit/ |respo-markdown.calcit/ |reel.calcit/ |respo-router.calcit/ |docs-workflow/ |js-ffi/
:type-slots $ {}
:type-slots $ {} $ :dispatch-op 'Dynamic
:files $ {}
'app.config $ %{} 'FileEntry
:defs $ {}
Expand Down
14 changes: 7 additions & 7 deletions deps.cirru
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

{} (:calcit-version |0.30.1)
{} (:calcit-version |0.31.0)
:version |0.0.4
:dependencies $ {} (|Respo/reel.calcit |0.6.41)
|Respo/respo-markdown.calcit |0.4.59
|Respo/respo-router.calcit |0.8.37
|Respo/respo-ui.calcit |0.8.0
|Respo/respo.calcit |0.16.124
|calcit-lang/docs-workflow |0.0.37
:dependencies $ {} (|Respo/reel.calcit |0.7.0-alpha.2)
|Respo/respo-markdown.calcit |0.5.0-alpha.3
|Respo/respo-router.calcit |0.9.0-alpha.1
|Respo/respo-ui.calcit |0.9.0-alpha.2
|Respo/respo.calcit |0.17.0-alpha.2
|calcit-lang/docs-workflow |0.0.38
|calcit-lang/js-ffi |0.2.5
2 changes: 1 addition & 1 deletion docs/beginner-guide.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
> Historical tutorial: the old macro expansions, JavaScript access and dispatch
> examples below predate current typed APIs. Start with **Guide** in the sidebar
> for formal 0.30.1 contracts. This is not a standalone checked current app.
> for formal 0.31.0 contracts. This is not a standalone checked current app.

Want to explore by yourself?

Expand Down
26 changes: 15 additions & 11 deletions docs/guide.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Guide: Calcit 0.30.1
# Guide: Calcit 0.31.0

This site pairs formal Calcit CLI / `@calcit/procs` **0.30.1**, Respo
**0.16.124**, Reel **0.6.41** and Docs Workflow **0.0.37**. Use `calcit.cirru`
This site pairs formal Calcit CLI / `@calcit/procs` **0.31.0**, Respo
**0.17.0-alpha.2**, Reel **0.7.0-alpha.2** and Docs Workflow **0.0.38**.
The CLI/runtime are formal releases; the module graph still includes prereleases. Use `calcit.cirru`
for source and `deps.cirru` for published module versions; do not restore
`compact.cirru` or `package.cirru`, or mix this runtime with an alpha CLI.

Expand All @@ -13,7 +14,7 @@ versions before copying code: upgrading this site does not upgrade other apps.

Use `defref` / `ref` for retained mutable references, and `add-watch!` /
`remove-watch!` for effectful subscriptions. The historical `defatom`, `atom` and
unmarked watch names are not recommended spellings. Do not hide a missing type
unmarked watch names have been removed from the current language. Do not hide a missing type
relationship with `unsafe-coerce` merely to pass a check.
Comment thread
tiye marked this conversation as resolved.
Resolved

A native counter illustrates an explicit effectful function contract:
Expand All @@ -36,12 +37,14 @@ strings and numbers through published JS-FFI adapters; an unchecked cast does
not prove a JavaScript value has a Calcit schema. Keep JavaScript `bottom-tip`
`hud!` calls in development reload handlers so errors remain visible on-page.

## Component operations are not application operations
## Event callbacks and application operations

`respo.core/>>` selects a state branch and carries its cursor. Widget handlers
receive Respo's widget dispatcher; an application `render!` dispatcher receives
one operation. Do not copy the two-argument widget dispatcher's type into the
application entry point.
`respo.core/>>` selects a state branch and carries its cursor. A Respo event
handler receives `(event, dispatch!)`; the dispatch function accepts exactly
one value of the entry's `dispatch-op` type and returns Unit. For state changes,
package the cursor and new state in one application operation, then validate
its fields at the app boundary. A two-argument handler or custom
`on-select(path, d!)` callback does not make `d!` a two-argument dispatcher.

Define an application Op enum and validate incoming state cursors/payloads
before constructing it. With typed Reel, retain `State<Op, Store>` in a Ref,
Expand All @@ -68,8 +71,9 @@ checks, full strict diagnostics, and zero deprecated/dynamic-method findings in
CI. Both SPA and mdBook are built. These checks do **not** execute every Markdown
fragment or certify browser/WebSocket application behavior.

The Beginner Guide and several API chapters are historical design notes. Their
The Beginner Guide, older implementation discussions under Guide, and several API
chapters retain historical design notes. Their
old macro expansions, direct JavaScript access and operation syntax are not
copy-ready 0.30.1 code. Consult the paired published module's `calcit query def`
copy-ready 0.31.0 code. Consult the paired published module's `calcit query def`
and `query schema` views and the maintained template. Historical examples need
section-by-section migration, not mechanical renaming without contract checks.
173 changes: 40 additions & 133 deletions docs/guide/component-states.md
Original file line number Diff line number Diff line change
@@ -1,151 +1,58 @@
## Component States

Unlike React, states in Respo is maintained manually for stablility during hot code swapping.
At first, states is a HashMap inside the store:
A component state branch is an open Map with a `:cursor` path and optional
`:data`. The state value can differ between widgets. Keep that heterogeneous
boundary explicit; decode the fields a widget consumes before using them as a
business model. A missing Map key returns `Option :none`, not an implicit nil.

```cirru
defatom *store {}
:states $ {}
```

By design, if states is added, you would a tree:

```cirru
{}
:states $ {}
:data $ {}
:todolist $ {}
:data $ {}
:input "|xyz..."
"task-1-id" $ {}
:data $ {}
:draft "|xxx..."
"task-2-id" $ {}
:data $ {}
:draft "|yyy..."
"task-2-id" $ {}
:data $ {}
:draft "|zzz.."
```

`:data` is a special field for holding state of each component.
It has to be a tree since Virtual DOM is a tree.
You also notice that its structure is simpler than a DOM tree, it only contains states.

`respo.core/>>` is the "picking branch" function. It also maintains a `:cursor` field.

When you call `(>> states :todolist)`, you get new `states` variable for a child component:

```cirru
{}
; "generated cursor, nil at top level"
:cursor $ [] :todolist
; "state at current level"
:data $ {}
:input "|xyz..."

; states for children
"task-1-id" $ {}
:data $ {}
:draft "|xxx..."
"task-2-id" $ {}
:data $ {}
:draft "|yyy..."
"task-2-id" $ {}
:data $ {}
:draft "|zzz.."
```

Then you call `(>> states "task-1-id")` and you get new `states` for child "task-1":

```cirru
{}
; "generated cursor"
:cursor $ [] :todolist "|task-1-id"

; "state of task-1"
:data $ {}
:draft "|xxx..."
```

For state inside each component, it's `nil` at first.
You want to have an initial state, use `or` to provide one.
`respo.core/>>` selects a branch and extends its cursor. This small example uses
Tag keys, matching this documentation app's checked `List<Tag>` operation path:

```cirru
defcomp comp-task (states)
let
cursor (:cursor states)
state $ or (:data states) $ {}
:draft "|empty"
(div {}))
let
states $ {} (:cursor $ []) (:task $ {} (:data |Draft))
branch $ respo.core/>> states :task
assert= (Option :some $ [] :task) (get branch :cursor)
assert= (Option :some |Draft) (get branch :data)
```

By accessing `(:data states)`, you get `nil`, so `&{} :draft "|empty"` is used.
After there's data in states, you get data that was set.
A missing branch starts with an empty cursor-bearing Map. Do not replace a
present false, zero or empty string state with an arbitrary default. Match the
Option returned by `get`; decode a present value according to the widget's
actual state shape.

Then you want to update component s:tate
A Respo event handler receives two arguments: the event Map and the application
dispatch function. That dispatch function accepts **one** entry `dispatch-op`
value and returns Unit. Two handler arguments do not imply a two-argument
state dispatcher. Wrap the cursor and new state in the app's single operation:

```cirru
defcomp comp-task (states)
let
cursor (:cursor states)
state $ or (:data states) $ {}
:draft "|empty"
div $ {}
:on-click $ fn (e dispatch!)
dispatch! cursor (assoc state :draft "|New state")
fn (event d!)
d! $ :: :states cursor new-state
, &unit
```

So `(dispatch! cursor state)` sends the new state.
This is a callback fragment in a context that already provides `cursor` and
`new-state`, not a standalone program. This site's application boundary checks
the raw Enum and payload before constructing the published nominal Op.
A custom callback such as `on-select(path, d!)` can also have two arguments;
that does not change the arity of the `d!` value passed to it.

The last step to to update global states with `respo.cursor/update-states`.
Internally `(dispatch! cursor op-data)` will be transformed to `(dispatch! :states ([] cursor op-data))`.
And then in `updater` you add:
For the tree update itself, use the current three-argument helper:

```cirru
case-default op
; other actions
do store

; "where op-data is [cursor new-state]"
:states (update-states store op-data)
let
updated $ respo.cursor/update-state-tree ({}) ([] :task) |Edited
assert= ({} $ :task $ {} (:data |Edited)) updated
```

---

Let's wrap it. First we have empty states inside store:

```cirru
{}
:states $ {}
```

And it is passed to `(comp-todolist (>> states :todolist) data)`,
and then passed to `(comp-task (>> states (:id task)) task)`.

In `comp-todolist`, `(:data states)` provides component state, `(:cursor states)` provides its cursor.
Call `(dispatch! cursor {:input "|New draft"})` and global store will become:

```cirru
{}
:states $ {}
:todolist $ {}
:data $ {}
:input "|New draft"

```

In `comp-task` of "task-1", you also get `state` and `cursor`, so call `(dispatch! cursor {:draft "New text"})` you will get:

```cirru
{}
:states $ {}
:todolist $ {}
:data $ {}
:input "|New draft"
"task-1-id" $ {}
:data $ {}
:draft "|New text"
```
`update-state-tree` and `respo.cursor/update-states` expose open boundaries;
`update-states` also takes three arguments: store, cursor, new state. Their
results are not automatically proof of a nominal Store. This app's published
Docs Workflow updater checks the updated state Map before assigning it into
Store. Its app dispatcher validates cursors and hydrated stores before building
`docs-workflow.schema/Op`, then typed Reel records that operation.

And that's how Respo states is maintained.
Use the actual checked `app.main/dispatch!` and `docs-workflow.updater/updater`
definitions as integration references. The removed `case-default` example and
the old two-argument `update-states` call are not current alternatives.
50 changes: 33 additions & 17 deletions docs/guide/hot-swapping.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
## Hot swapping

Hot swapping is done by the compiler. What you need to do is to call `respo.core/clear-cache!` before re-rendering happens:
Use retained `defref` values and explicit watch effects. The current app keeps
its typed Reel in `app.main/*reel`; its value is immutable, while the reference
is mutable. `defatom`, `atom`, `add-watch` and `remove-watch` are retired names.
Do not add another reference around the initial store.

This is the initialization expression from this site's checked source:

```cirru
defn reload! ()
; "clear component caches"
clear-cache!
; "rerender DOM tree, I mean, a diff/patch loop"
render-app!
println "|Code update."
defref *reel $ typed/new-reel schema/store
```

If you don't, in the next rendering phase old element tree would be used if no argument changes found, which means Respo would still use render functions defined previously.
It belongs with the published `reel.typed/State<Op, Store>` schema attached to
`app.main/*reel`. An isolated excerpt does not carry that schema or its imports;
inspect the actual definition with `calcit query def 'app.main/*reel'` before reuse.

In Respo, you are asked to define `*store` explicitly. They the global states of data. As an Atom, the value inside is immutable, but the reference is mutable. During hot swapping, variables defined with `defatom` will be retained. As a result, component states are persistent even code is swapped:
The successful branch of this app's `reload!` uses the following sequence:

```cirru
defatom *store $ atom
or
let
raw $ or
js/localStorage.getItem "|respo"
, "|{:data [], :states {}}"
read-string raw
, schema/store
remove-watch! *reel :changes
clear-cache!
add-watch! *reel :changes $ fn (reel prev)
render-app!
reset! *reel $ typed/refresh updater @*reel schema/store
```

Clear the component cache before refreshing Reel. Register the new callback
before that refresh so its state change renders the new code. Watch callbacks
receive both the new and previous values; they are not the application dispatch
callback. Replacing the named watch avoids retaining an obsolete callback after
reload.

Keep the original error branch and JavaScript `bottom-tip` `hud!` calls: build
errors must remain visible on the page. A failed build must not refresh the
running state as though it were valid. `hud!` is a host UI function, not a
replacement for `raise` or a language primitive.

Archive loading is a separate boundary. This app currently writes an archive
but does not restore it at startup; this migration does not enable restoration.
If adding restoration, decode the wire value into the local Store before
registering automatic saves. A parse failure must not overwrite the original
archive with a default store.
Loading