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
55 changes: 54 additions & 1 deletion crates/vertigo-cli/src/build/wasm_opt.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
use std::{fs, process::Command};
use std::{fs, process::Command, sync::OnceLock};

use super::wasm_path::WasmPath;

/// Features that rustc enables by default for the `wasm32-unknown-unknown` target.
///
/// Normally `wasm-opt` picks these up from the `target_features` custom section emitted
/// by LLVM, but that section is dropped whenever the binary is stripped
/// (`strip = true` in the cargo profile, which is a very common setting for wasm builds).
/// Without it `wasm-opt` falls back to its own default feature set and refuses to validate
/// the module, e.g.:
///
/// ```text
/// [wasm-validator error in function 1765] unexpected false: memory.copy operations
/// require bulk memory operations [--enable-bulk-memory-opt]
/// ```
///
/// So we always pass them explicitly.
const WASM_FEATURES: &[&str] = &[
"--enable-bulk-memory",
"--enable-bulk-memory-opt",
"--enable-nontrapping-float-to-int",
"--enable-sign-ext",
"--enable-mutable-globals",
"--enable-reference-types",
"--enable-multivalue",
];

pub fn run_wasm_opt(from: &WasmPath, to: &WasmPath) -> bool {
let from_str = from.as_string();
let to_str = to.as_string();

let mut wasm_opt_command = Command::new("wasm-opt");
wasm_opt_command.args(supported_features());
wasm_opt_command.args(["-Os", "--strip-debug", "-o", &to_str, &from_str]);

log::info!("Running: {wasm_opt_command:?}");
Expand Down Expand Up @@ -43,6 +68,34 @@ pub fn run_wasm_opt(from: &WasmPath, to: &WasmPath) -> bool {
}
}

/// Subset of [`WASM_FEATURES`] understood by the installed `wasm-opt`.
///
/// Older Binaryen releases reject unknown options, so flags missing from `--help`
/// are filtered out instead of failing the whole optimization step.
fn supported_features() -> &'static Vec<&'static str> {
static FEATURES: OnceLock<Vec<&'static str>> = OnceLock::new();

FEATURES.get_or_init(|| {
let help = Command::new("wasm-opt").arg("--help").output();

let help = match help {
Ok(output) => {
let mut help = String::from_utf8_lossy(&output.stdout).into_owned();
help.push_str(&String::from_utf8_lossy(&output.stderr));
help
}
// wasm-opt is missing or unusable - run_wasm_opt will report it
Err(_) => return Vec::new(),
};

WASM_FEATURES
.iter()
.copied()
.filter(|feature| help.contains(feature))
.collect()
})
}

fn size(path: &str) -> u64 {
fs::metadata(path)
.map(|md| md.len() / 1_024)
Expand Down
6 changes: 4 additions & 2 deletions crates/vertigo-macro/src/wasm_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ impl WasmPath {
use std::fs::File;
use std::io::prelude::*;

let mut f = File::create(&self.path)?;
f.write_all(content)?;
let mut f = File::create(&self.path)
.map_err(|err| format!("Can't create {}: {err}", self.as_string()))?;
f.write_all(content)
.map_err(|err| format!("Can't write to {}: {err}", self.as_string()))?;
Ok(())
}

Expand Down
15 changes: 14 additions & 1 deletion crates/vertigo/build.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::env;
use std::error::Error;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};

fn main() -> Result<(), Box<dyn Error>> {
let target_dir = PathBuf::from(env::var("OUT_DIR")?).join("../../..");
let target_dir = find_target_dir()?;

let _ = fs::remove_dir_all(target_dir.join("tailwind"));

Expand Down Expand Up @@ -42,6 +43,18 @@ fn main() -> Result<(), Box<dyn Error>> {
Ok(())
}

fn find_target_dir() -> Result<PathBuf, Box<dyn Error>> {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);

let target_dir = out_dir
.ancestors()
.find(|dir| dir.file_name() == Some(OsStr::new("build")))
.and_then(Path::parent)
.ok_or_else(|| format!("Can't find target dir in OUT_DIR: {}", out_dir.display()))?;

Ok(target_dir.to_path_buf())
}

fn bundle_file(
in_path: &str,
content: &str,
Expand Down
163 changes: 163 additions & 0 deletions crates/vertigo/docs/collection-key-and-list-renderers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# `CollectionKey` and the memoized list renderers

This document explains a small cluster of related building blocks in Vertigo and
how they fit together:

- [`CollectionKey`](#collectionkey) — a marker trait describing how to identify
items in a list (the key) and what the item type is.
- [`render_list_memo`](#render_list_memo) and
[`render_resource_list_memo`](#render_resource_list_memo) — high-level helpers
that render reactive lists while **memoizing each item**, so only items that
actually changed are re-rendered.

Per-item [`Computed`](crate::Computed)s come from
[`keyed_computed_list`](crate::keyed_computed_list).

```text
Source of truth keyed_computed_list Per-item reactive view Renderer
┌──────────────────┐ ┌────────────────────┐ ┌───────────────────────────┐ ┌──────────────────────────┐
│ Value<Rc<Vec<T>>>│ │ keyed_computed_list│ │ Vec<KeyedListItem> │ │ render_list_memo │
│ or │ ──────▶ │ │──▶ │ each item = Computed<V> │──▶│ / │
│ LazyCache<Vec<T>>│ graph │ │ │ │ │ render_resource_list_memo│
└──────────────────┘ └────────────────────┘ └───────────────────────────┘ └──────────────────────────┘
```

---

## `CollectionKey`

```rust,ignore
pub trait CollectionKey {
type Key: Eq + Hash + Clone + std::fmt::Debug + 'static;
type Value: Clone + PartialEq + 'static;
fn get_key(val: &Self::Value) -> Self::Key;
}
```

[`CollectionKey`](crate::CollectionKey) is a **marker / descriptor trait**. You implement it on a
zero-sized marker type (not on the item itself), and it declares three things:

- `Value` — the item type stored in the list.
- `Key` — a stable identity for an item (e.g. a database id).
- `get_key` — how to extract the key from an item.

Typical implementation:

```rust,ignore
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Item { pub id: u32, pub name: String }

pub struct ItemKey; // marker type

impl CollectionKey for ItemKey {
type Key = u32;
type Value = Item;
fn get_key(val: &Item) -> u32 { val.id }
}
```

The marker type (`ItemKey`) is the generic parameter `T` threaded through
[`LazyListCache<T>`](crate::LazyListCache) and the memoized list renderers. Keying matters for two reasons:

1. **Memoization** — items keep the *same* per-item `Computed` across
updates as long as their key is stable. Only items whose content actually
changed cause a re-render.
2. **Deduplication** — [`keyed_computed_list`](crate::keyed_computed_list) logs an error and skips
items with a duplicate key within a single list.

---

## `render_list_memo`

```rust,ignore
pub fn render_list_memo<T: CollectionKey + 'static>(
value: &Value<Rc<Vec<T::Value>>>,
render: impl Fn(&Computed<T::Value>) -> DomNode + 'static,
) -> DomNode
```

Renders a reactive list from a `Value<Rc<Vec<Item>>>`, memoizing each item.

Internally it maps the source to `Computed<Vec<Item>>` and calls
[`render_list`](crate::render::render_list), which runs
[`keyed_computed_list`](crate::keyed_computed_list) so each key keeps a stable
per-item [`Computed`](crate::Computed).

Because the render closure receives a `&`[`Computed`](crate::Computed)`<T::Value>` (not a bare value),
the rendered subtree for an item re-runs only when that item changes — not when
sibling items or the list order change.

Use this when **your list already lives in a [`Value`](crate::Value)** that you own.

---

## `render_resource_list_memo`

```rust,ignore
pub fn render_resource_list_memo<T: CollectionKey + 'static>(
value: &LazyCache<Vec<T::Value>>,
render: impl Fn(&Computed<T::Value>) -> DomNode + 'static,
) -> DomNode
```

Identical in shape to [`render_list_memo`](crate::render::render_list_memo), but the source is a
[`LazyCache`](crate::LazyCache)`<Vec<Item>>` — a lazily-loaded, possibly remote resource that
auto-refreshes on a TTL. Loading / Error states are normalized to an empty list,
then the cache is fed through [`keyed_computed_list`](crate::keyed_computed_list)
exactly like `render_list_memo`.

Use this when **your list comes from a fetched resource** and you still want
per-item memoization.

> Related: [`LazyListCache<T>`](crate::LazyListCache) is a higher-level wrapper (also keyed by
> [`CollectionKey`](crate::CollectionKey)) that adds optimistic create/update/delete and per-item
> fetching on top of a list resource. `render_resource_list_memo` is the
> lower-level renderer for a plain `LazyCache<Vec<Item>>`. See the
> [`LazyListCache` guide](crate::guides::lazy_list_cache).

---

## End-to-end example

```rust,ignore
use std::rc::Rc;
use vertigo::{dom, CollectionKey, Computed, DomNode, Value};
use vertigo::render::render_list_memo; // path-dependent; see "Public surface"

#[derive(Clone, PartialEq, Eq, Debug)]
struct Item { id: u32, name: String }

struct ItemKey;
impl CollectionKey for ItemKey {
type Key = u32;
type Value = Item;
fn get_key(v: &Item) -> u32 { v.id }
}

fn view(items: &Value<Rc<Vec<Item>>>) -> DomNode {
render_list_memo::<ItemKey>(items, |item: &Computed<Item>| {
let item = item.clone();
item.render_value(|it| dom! { <div>{it.name}</div> })
})
}
```

When `items` is updated:

- items with unchanged content are **not** re-rendered (their inner `Computed`
did not change),
- only added / removed / reordered / mutated items cause DOM work.

---

## Public surface (what you can use directly)

| Item | Exported as |
| ----------------------------- | ---------------------------------------- |
| [`CollectionKey`](crate::CollectionKey) | `vertigo::CollectionKey` |
| [`render_list_memo`](crate::render::render_list_memo) | `vertigo::render::render_list_memo` |
| [`render_resource_list_memo`](crate::render::render_resource_list_memo) | `vertigo::render::render_resource_list_memo` |

[`render_list_memo`](crate::render::render_list_memo) / [`render_resource_list_memo`](crate::render::render_resource_list_memo) are reachable through the public
`render` module (`vertigo::render::…`); they are not re-exported at the crate
root the way [`CollectionKey`](crate::CollectionKey) is.
2 changes: 1 addition & 1 deletion crates/vertigo/docs/lazy-list-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ where you don't need per-item granularity, prefer
`T` is a **marker type** implementing [`CollectionKey`](crate::CollectionKey).
It declares the item type (`T::Value`) and how to derive a stable key from it
(`T::Key`, via `T::get_key`). See the
[companion guide](crate::guides::value_synchronize_and_collections) for the
[companion guide](crate::guides::collection_key_and_list_renderers) for the
full rationale.

Internally the cache holds, per visible item, a `ListItem<V>` with **two**
Expand Down
Loading
Loading