Skip to main content

CHANGELOG.md

# Changelog

All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.3.0-rc.0] — 2026-05-24

This release is a substantial overhaul of the DSL and template surface from
0.2.0. It collapses several legacy paths into the modern `calculate` /
`rx()` / `~L` model, introduces a non-DSL on-ramp (`Lavash.LiveView.Explicit`),
and adds compile-time machinery (auto-injection, diagnostics, optimistic
JS hook integration) that lets the user write near-vanilla Phoenix HEEx
with reactive behavior wired in for free.

### Breaking changes

- **Removed the `derive` DSL block.** `derive :name do argument ... run/compute end`
  no longer compiles. Use `calculate :name, rx(...)` instead — the modern
  form already does what `derive` did, plus transpiles to JS for the
  optimistic path. Migration is mechanical: convert each `argument`
  declaration to a state/derive reference inside the `rx(...)` body.
- **Removed the `update :field, fn` action operation.** Use
  `set :field, rx(...)` instead — same Elixir semantics plus client-side
  transpilation. Mechanical migration:
  - `update :count, &(&1 + 1)``set :count, rx(@count + 1)`
  - `update :flag, &(!&1)``set :flag, rx(not @flag)`
  - `update :n, fn n -> (n || 0) + 1 end``set :n, rx((@n || 0) + 1)`
- **Removed the `notify_parent :event` action operation.** No usage anywhere
  in this repo's demo or fixtures, and `bind=` + PubSub cover the
  state-sync and cross-process cases. For the genuine "child fires a
  callback on its parent" case, use `send(self(), {:my_event, payload})`
  inside a `run fn` and pattern-match in `handle_info/2`.
- **Removed `Lavash.Argument`** (the derive-specific argument struct).
  Read-side arguments use `Lavash.Read.Argument` and are unaffected.
- **Removed `Lavash.Actions.Update`** and `Lavash.Actions.NotifyParent`
  structs, along with their runtime helpers.
- **Removed the `:lavash_component_*` message tag family** (`_set`, `_close`,
  `_delta`, `_add`, `_remove`, `_toggle`) in favor of a single
  `{:lavash_field_op, op, field, value}` envelope. The `_delta`/`_close`/etc.
  handlers were dead code anyway; the `_set` handler is now reached via
  the new envelope.
- **`~L` is required** for Lavash LiveView and Component render functions —
  `~H` works but skips the lavash template transformer (no auto-injection,
  no diagnostics). Existing code should already be on `~L`; this is a
  documentation clarification rather than a runtime change.
- **Test fixtures renamespaced.** `Lavash.Test*` modules in `test/support/`
  moved to `Lavash.Test.Magic.*` (DSL fixtures) and a new
  `Lavash.Test.Explicit.*` namespace (plain Phoenix.LiveView fixtures).
  Demo applications consuming these fixtures (none expected) need to update.

### Added

- **`Lavash.LiveView.Explicit`** — non-DSL entry point. `use
  Lavash.LiveView.Explicit` + a `reactive do state ... derive ... end`
  block gets you the reactive graph with `put_state/3`, automatic mount,
  and async-derive dispatch, but no template transformer, no
  optimistic JS hook, no forms/overlays/bindings. For "I want the
  reactive engine but not the rest."
- **Compile-time diagnostics** in the `~L` template transformer:
  - Warn when a bare `{@field}` references a declared-but-non-optimistic
    state field (likely missing `optimistic: true`).
  - Warn when `bind={[child: :parent]}` targets a `:parent` that isn't a
    declared state field on the host.
- **Auto-injection** of more `data-lavash-*` attributes from natural
  Phoenix HEEx patterns:
  - `class={if @bool, do: A, else: B}` on an optimistic boolean →
    auto-injects `data-lavash-toggle`.
  - `class={if val in @list, do: A, else: B}` on an optimistic array →
    auto-injects `data-lavash-member` + `data-lavash-member-value`.
  - `<.lavash_component bind={[n: :p]}>` → auto-injects `n={@p}` so the
    child receives the parent's value.
- **Compile-time graph cache invalidation.** `Lavash.Dsl.Graph`,
  `Lavash.Reactive`, and `Lavash.Rx.Cache` register `@after_compile`
  hooks that drop their `:persistent_term` cache entries when a Lavash
  module recompiles in dev. Renamed fields and removed calculations
  no longer carry stale compute fns across reloads.
- **AST eval cache** (`Lavash.Rx.Cache`) — user-supplied ASTs in `rx()`
  expressions and `run` functions are compiled once and stored in
  `:persistent_term`, replacing the per-fire `Code.eval_quoted` cost.
- **Browser-driven integration tests** via Wallabidi + Lightpanda. 61
  end-to-end tests cover counters, bindings, DOM directives, forms,
  arrays, overlays, optimistic state, and reconnects against real
  Lightpanda. The lavash JS hook is loaded into the test layout.
- **`Lavash.LiveView.Compiler.collect_optimistic_fields/1`** — public
  accessor for "state fields that should be optimistic" (both
  explicitly declared and implicitly via action-touched).
- **`Lavash.State.MissingRequiredFieldError`** structured exception
  replacing the old bare-string raise in URL field hydration.
- **`Lavash.Form.Runtime.extract_submit_errors/1`** — single home for
  the Ash-error-to-field-error-map conversion that was duplicated in
  five places.
- **`Lavash.Type.decode_wire/1`** — JS-wire decoder unifying the
  previously-duplicated `parse_value/1` and `parse_binding_value/1`.
- **`Lavash.Component.CompilerHelpers.resource_available?/1`**  unified the previously-duplicated Ash resource compile-readiness check.
- **`__lavash__(:declared_actions)` and `__lavash__(:derives)`**  introspection accessors for callers that need the un-augmented
  user-declared entities (vs. the `:actions` accessor which also
  includes synthetic setters and optimistic actions).

### Changed

- **Bindings are now bidirectional.** The template transformer
  auto-injects the parent's bound-field value into the child's assigns,
  so the child sees parent updates as well as propagating its own
  writes back. Sibling components bound to the same parent field stay
  in sync via the parent.
- **User callbacks are overridable.** `handle_params`/`handle_event`/
  `handle_info` on LiveViews and `update`/`handle_event` on Components
  now have `defoverridable` declared, so users can write their own
  clauses and call `super/N` to fall through to the lavash dispatch.
- **`:lavash_*` catch-all logs.** The `handle_info` catch-all in
  `Lavash.LiveView.Runtime` logs a warning when an unrecognized
  `:lavash_*` tuple arrives instead of silently dropping it. Library
  bugs surface in the log instead of vanishing.
- **`component_states` routed through assigns**, not the process
  dictionary. The per-component-id state map (used to restore socket
  state across reconnects) now travels via `@__lavash_component_states__`
  and propagates through nested `<.lavash_component>` calls. Visible
  in change tracking and in tests.
- **Three DSL metadata access patterns reduced to a clear partition.**
  `Transformer.get_entities` for compile time, `module.__lavash__/1`
  for the canonical runtime accessor, and `Spark.Dsl.Extension.get_entities/2`
  as an escape hatch for the few callers that need un-augmented
  entities.
- **Phoenix.HTML.FormData protocol on `Lavash.Form`** now implements
  `to_form/4`, `input_value/3`, and `input_validations/3`. Nested form
  inputs (`<.inputs_for>`) now work end-to-end.
- **Default Elixir/OTP versions pinned.** `.tool-versions` declares
  `elixir 1.19.0-otp-28` / `erlang 28.1`.
- **Pre-commit hook ships in `.githooks/`.** Opt in with
  `git config core.hooksPath .githooks`. Runs format, compile
  --warnings-as-errors, credo --strict, and the full test suite.
- **Credo strict clean.** The repo passes `mix credo --strict` with no
  findings; the pre-commit hook keeps it that way.

### Fixed

- **`apply_runs/4` was silently no-op for any non-initial action.**
  `Phoenix.Component.assign` stores either `true` (initial render) or
  the *old value* (subsequent change) in `__changed__`; the old code
  pattern-matched only on the literal `true` and dropped every
  subsequent run-based action's writes. Now it accepts any marker.
- **`inside_display_element?` mis-tracked depth for void elements.**
  `<input>`, `<br>`, etc. produce a `:tag` token with no matching
  `:close`, so they wrongly consumed the depth-0 slot when scanning
  back through the accumulator. Now skipped via `closing: :void` /
  `closing: :self` in tag meta.
- **`Rx.qualify_local_calls` broke imported helpers.** Bare calls were
  rewritten to `Caller.fn(...)` regardless of whether the name was
  imported from another module. Now consults `__CALLER__.functions` /
  `.macros` and qualifies against the import's source module.
- **Silent `try/rescue` around `apply_submits` removed.** Used to swallow
  every exception, set a `[DEBUG] Exception in submit` flash, and
  return `{:ok, socket}` — bypassing the user's `on_error` action.
  Now unexpected exceptions crash the LiveView per Phoenix conventions;
  validation failures still route through `{:error, form_with_errors}`
  to `on_error`.
- **`optimistic_state/2` no longer evaluates async calculations
  synchronously.** Calling the user's `slow_fn` inside render blocked
  mount and bypassed the AsyncResult flow.
- **Reactive graphs are no longer evaluated stale after recompile.**
  See "Added: Compile-time graph cache invalidation."

### Removed

- Files: `lib/lavash/argument.ex`, `lib/lavash/actions/update.ex`,
  `lib/lavash/actions/notify_parent.ex`.
- DSL entities: `:derive`, `:update` (the action op, not Ash's update),
  `:notify_parent`.
- The `<.o>` display helper (deprecated long ago; `~L` auto-wraps bare
  `{@field}` instead).
- Pre-DSL `data-optimistic-*` attribute family (`data-optimistic`,
  `-display`, `-field`, `-value`). The current API is `data-lavash-*`,
  auto-injected by `~L`.
- The stale "Architecture" section in README that described
  `socket.private.lavash` as the state store — state lives in
  `socket.assigns` eagerly written by `LSocket.put_state/3`.
- Five duplicate copies of `extract_submit_errors/1`, two of
  `parse_value`/`parse_binding_value`, two of `resource_available?/1`.
- The process-dictionary side channel for `component_states`.

[Unreleased]: https://github.com/u2i/lavash/compare/v0.3.0-rc.0...HEAD
[0.3.0-rc.0]: https://github.com/u2i/lavash/compare/v0.2.0...v0.3.0-rc.0