ripple@0.4.0
Ripple-TS/rippleripple@0.4.0Sep 15, 2026by github-actions[bot]
Upgrading?
AI Summary
Minor release removing lazy destructuring syntax and reworking the reactive data model, alongside client-side performance optimizations for rendering and effects.
Key Highlights
- Removed lazy destructuring syntax (`&[...]`) in favor of explicit `.value` access
- Props are now plain objects with no Proxy or special helpers
- Lighter DOM output for lists and `@if` blocks to reduce anchor nodes
- Cheaper element refs and effects implementation on the client
Breaking Changes
- Lazy destructuring syntax (`&[...]` and `&{...}`) is now a syntax error
- Tracked, Derived, and WritableDerived are plain `{ value: V }` shapes (removed tuple member and runtime accessors)
- Props system removed `Props.keys`, `Props.values`, etc., and no longer uses Proxy/spread lowering
- Removed `lazy_array_*` helpers and `fallback`, `exclude_from_object`, `array_slice` helpers
New Features
- Custom transport handlers for hydration
- Browser entry for config helpers
- Enhanced primitive text inference
Full Release Notes
### Minor Changes
- [#1472](https://github.com/Ripple-TS/ripple/pull/1472)
[`5d3e60d`](https://github.com/Ripple-TS/ripple/commit/5d3e60d86129ac20d23c86a923914ee44efea9d1)
Thanks [@leonidaz](https://github.com/leonidaz)! - Lazy destructuring is removed
from the language (tsrx RFC
[#106](https://github.com/Ripple-TS/ripple/issues/106)). `&[ ... ]` and
`&{ ... }` are syntax errors now; a tracked value is read and written through
`.value`, and a `Tracked`, `Derived`, or `WritableDerived` object is passed to a
child as it is (`<Child {count} />`, or `count={track(() => count.value)}` for a
read-only view).
| Before | After |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `let &[count] = track(0); count++; {count}` | `const count = track(0); count.value++; {count.value}` |
| `let &[count, countT] = track(0); <Child count={countT} />` | `const count = track(0); <Child {count} />` |
| `let &[double] = track(() => count * 2)` | `const double = track(() => count.value * 2)` |
| `function Card({ count: &[count] }: { count: Tracked<number> })` | `function Card({ count }: { count: Tracked<number> })` and `count.value` |
`Tracked<V>`, `Derived<V>`, and `WritableDerived<V>` are plain `{ value: V }`
shapes; the `[V, Tracked<V>]` tuple member, the runtime `[0]`/`[1]` accessors,
and the `lazy_array_*` helpers are gone, and `count[0]` reports the ordinary
"use `.value`" error. Rest and default patterns lower to native destructuring: a
keyed `@for` pattern with a rest element or a default is destructured once per
item change, and a destructuring assignment onto boxed `let`s writes the boxes
in place; the `fallback`, `exclude_from_object`, and `array_slice` runtime
helpers are gone. On the server a dynamic tag `<{tag} />` is lowered to
`_$_.dynamic_element(tag, props)`, with the tag and the element's own props
passed separately, so the props never carry an `is` entry.
### Patch Changes
- [#1458](https://github.com/Ripple-TS/ripple/pull/1458)
[`bbc1445`](https://github.com/Ripple-TS/ripple/commit/bbc14455485e054c145ebc78f08a771d41ea16f1)
Thanks [@leonidaz](https://github.com/leonidaz)! - Lighter list, `@if`, and
mount output on the client. A controlled `@for` (the sole content of its
element) appends its items into the parent instead of inserting before an empty
text anchor, and an `@if` at the root of such an item creates an anchor only
once it needs a position of its own (a branch swap, or a branch that renders
nothing), so a keyed list of `@if` items keeps no anchor nodes in the DOM;
`mount()` likewise renders into the target without an anchor node. An `@if`
block now renders its branch directly and owns the branch's DOM range, and a
list keeps its inputs in block state, so every `@if` and `@for` allocates one
block less and no closures. Text and class updates compare against the last
value in the render block's state instead of a cache on the DOM node; the
compiler emits that compared form for single updates as well.
A selector key whose subscribers all left stays in the selector's map for reuse
when the key comes back, and is swept once more than 1024 released keys pile up.
- [#1469](https://github.com/Ripple-TS/ripple/pull/1469)
[`b3a38ce`](https://github.com/Ripple-TS/ripple/commit/b3a38ce0aae66a9dcac0b663c30161eed0d7547a)
Thanks [@leonidaz](https://github.com/leonidaz)! - Add custom serialization for
trackAsync hydration and RPC arguments/results using `transport` handlers in
ripple.config.ts. Register matching encoders and decoders automatically in
development and production, and expose `setTransport` for custom integrations.
Apps without a transport retain the plain JSON hydration fast path. Provide a
browser entry for config helpers so importing defineConfig does not load the
Vite plugin's Node.js dependencies during hydration. A configured hydration
payload embeds devalue's flattened form directly, so the client revives it
without a second string encoding.
- [#1460](https://github.com/Ripple-TS/ripple/pull/1460)
[`2ef5da5`](https://github.com/Ripple-TS/ripple/commit/2ef5da52b26fc13e11537dd28df0e2b376ca0f52)
Thanks [@leonidaz](https://github.com/leonidaz)! - Cheaper element refs,
effects, and component templates on the client. A `ref` is one effect block
keyed on its state instead of a render block that re-evaluates the thunk and
creates a branch and an effect for it, since the ref value is read untracked and
fixed for the life of the enclosing block. Effects deferred until a component
has rendered are recorded as flat entries, and an effect's first run skips the
child and teardown sweep. A tracked value written during a flush keeps its
previous value for teardowns on itself instead of in a map, and `flushSync`
releases those values when it finishes (they were only released by the microtask
flush before).
The compiler types the bindings of a destructured pattern
(`{ item }: { item: Item }` as component props, or `const { item } = props` from
a typed initializer), so their property reads lower to direct text and attribute
writes. A component whose body has setup statements beside a single root element
clones that element instead of a fragment.
- [#1470](https://github.com/Ripple-TS/ripple/pull/1470)
[`0321791`](https://github.com/Ripple-TS/ripple/commit/03217912651e0a49edb8060a92602fd0e0abe193)
Thanks [@leonidaz](https://github.com/leonidaz)! - Component props are plain
objects, evaluated once. A call site such as `<Child a={x} b="y" />` compiles to
the object literal `{ a: x, b: 'y' }` and passes it to the component as is: no
getters, no props class, no Proxy for spreads, no `Props` helpers, and no
`Object.keys`/spread lowering, so props behave like any other object in `.tsrx`
and `.ts` code alike. A prop expression is evaluated when the component is
called, so a child that must follow a change now receives a live value
explicitly: a tracked object (`count={count}`) or a derived
(`title={track(() => label + count.value)}`), read through `.value`.
`count={count.value}` passes the current number. `track(fn)` is typed
`Derived<V>` with a read-only `value`; `track(fn, get, set)` or
`track(fn, undefined, true)` is a `WritableDerived<V>`, and a write to a
read-only derived warns in development. A dynamic element
(`<{tag} class={x} />`) still updates its attributes reactively. `Props.keys`,
`Props.values`, `Props.entries`, `Props.has`, `Props.spread`, `Props.rest`,
`Props.ownSymbolKeys` and `Props.ownAllKeys` are removed, as they are plain
`Object` operations now.
- [#1457](https://github.com/Ripple-TS/ripple/pull/1457)
[`020d269`](https://github.com/Ripple-TS/ripple/commit/020d2699f8e1333367ac9f7abe9247bab55e272d)
Thanks [@leonidaz](https://github.com/leonidaz)! - Fix hydration when a fragment
root or a slot is followed by siblings. The compiler's closing `next(n)` for a
fragment root now counts from wherever the cursor was last positioned, so a
trailing element navigated to for an event or attribute, tracked text, a
control-flow block, a component, or a `{expression}` no longer makes the cursor
overshoot or fall short of the fragment's last node; a trailing element that was
descended into is popped back to. Control-flow bodies nested inside an element
now emit that `next(n)` too. At runtime, a `{expression}` leaves the cursor on
its own end marker like every other block, and a multi-node branch or `@for`
item steps to its block's end marker after appending, so the next sibling, item,
or component adopts the right node.
- [#1462](https://github.com/Ripple-TS/ripple/pull/1462)
[`453bf4b`](https://github.com/Ripple-TS/ripple/commit/453bf4ba8b1b31963df26696545e446ab4b1658e)
Thanks [@leonidaz](https://github.com/leonidaz)! - Faster keyed lists and
lighter component output on the client. A `@for` keyed by the item itself
(`key item`) keys by identity with no key callback, so a re-run compares the
item arrays directly and skips the per-item value update; a same-length keyed
re-run rewrites the list's block array in place instead of allocating a new one,
with the unchanged prefix and suffix skipped by plain compares. Static child
components that follow their template siblings append into the parent element
instead of inserting before a `<!>` placeholder, so a component with trailing
child components clones fewer nodes and leaves no comment anchors in the DOM.
The compiler also infers a primitive text type through a call whose callee is
declared to return one (a binding typed as a function type, an `as` assertion, a
function with a declared return type, or a method or function-typed property of
an annotated object), so such text lowers to `set_text` instead of a generic
expression block.
- [#1463](https://github.com/Ripple-TS/ripple/pull/1463)
[`411809f`](https://github.com/Ripple-TS/ripple/commit/411809fb38f25b259a1078a901839318e4207871)
Thanks [@leonidaz](https://github.com/leonidaz)! - Faster cold hydration and
server rendering. `hydrate()` accepts `rootBoundary: false` like `mount()`,
stepping over the server's root marker itself instead of rendering under a `try`
block (a streamed shell whose root suspended still hydrates under the default
boundary, which alone can adopt and activate its slot). The client runtime
compiles less on a cold page: the hydration cursor steps live in one module and
assign the cursor directly, a list's first render never compiles the diff, a
template's clone path stays out of its hydrating closure, `run_block` keeps
re-run and teardown handling in separate functions, and the compiler adopts an
element's lone text child in place (`hydrate_text`) with no `pop()`. A `@for`
whose `index` nothing reads allocates no tracked index per item. Removing the
server's inline styles now starts from an animation frame rather than inside
`mount()`/`hydrate()`.
On the server, `render()` joins its buffer tree with a plain walk instead of
`flat(Infinity).join('')`, `escape()` returns strings without `&`, `<` or `"`
after a single regex test, and an `Output` allocates its stream, css and
async-operation collections only for the root or on first use.
- [#1453](https://github.com/Ripple-TS/ripple/pull/1453)
[`82bc9ee`](https://github.com/Ripple-TS/ripple/commit/82bc9ee585c6b273bf69890bc2f230aba922bf19)
Thanks [@leonidaz](https://github.com/leonidaz)! - Speed up portals and root
event delegation. `<Portal>` now lowers to a dedicated `portal()` runtime call,
portal content appends straight into its target without a placeholder node,
delegated root listeners are shared per target instead of per portal, event
registration no longer scans every root target, and `@if` blocks and compiled
`event()` calls allocate fewer closures. `mount()` accepts `rootBoundary: false`
to render without the default root try/pending boundary. Cold code paths
(keyed-list diff, block error handling, effect-phase flushing, hydration
branches) live in their own functions so a first mount compiles less. A
component whose root is `@if`, `@for`, `@switch`, or `@try` now re-renders at
its own position when it is portal content or one of an element's all-component
children, instead of at the end of the parent; `mount()`/`hydrate()` disposers
are safe to call more than once.
- [#1470](https://github.com/Ripple-TS/ripple/pull/1470)
[`0321791`](https://github.com/Ripple-TS/ripple/commit/03217912651e0a49edb8060a92602fd0e0abe193)
Thanks [@leonidaz](https://github.com/leonidaz)! - Cheaper component
instantiation and `@if` chains on the client. An `@else if`, and a branch whose
only statement is another `@if`, fold into the enclosing `@if` block on both the
client and the server, so a chain costs one block and one hydration boundary. An
`@if` whose condition reads no tracked state renders its branch directly under
the enclosing block with no block of its own (a block without dependencies is
never scheduled, so nothing is lost); a dynamic condition keeps its block and
adopts the dependencies recorded while it was evaluated. A module-level
component also carries a direct render entry so `render_component` skips the
element it would otherwise return. An `@if` condition and its branches compile
to module-level functions when they capture at most one local (passed through
the if runtime), a condition returns the branch function to render instead of
calling a selection callback, and a render block's function is module-level with
its captured locals carried on the block state, so a component instantiation
creates no closures for its ifs or its reactive text and attribute updates.
`Context.get()` walks a chain of the context entries that were set rather than
every ancestor component. A module-level component's render body is a
module-level function that receives the props from the element instead of a
closure created per instantiation, `Context.get()`/`set()` calls on a
`new Context()` binding skip the `with_scope` wrapper, and `mount()` only
schedules the SSR style removal when the page has server-rendered inline styles.
A `@switch` now lowers onto the same runtime as `@if`: its selector returns the
case to render, so a `@switch` over a value that reads no tracked state renders
its case with no block, its cases are module-level functions when they capture
at most one local, and the separate switch runtime is gone. The probe that
decides whether an `@if` needs a block now renders a static branch itself, one
runtime call instead of four. A module-level component with a destructuring
parameter gets the direct render entry as well, and an `@if` or `@switch` is
hoisted whatever number of locals it captures: several travel as one object
literal destructured in the hoisted signatures. A binding that hoisted template
code writes (a `ref={name}` setter, a handler that assigns it) or that is
reassigned after template code reads it is compiled to a `{ v }` box, so the
hoisted functions share the variable with the component exactly as a closure
would: a plain `let`, a name inside a destructuring pattern of any shape, a
function or catch parameter (reboxed first thing in the body), and a `let`
written through a destructuring or `for...of` assignment target.
- [#1473](https://github.com/Ripple-TS/ripple/pull/1473)
[`eb638ea`](https://github.com/Ripple-TS/ripple/commit/eb638ea4a3c73b7ecb98ccedfffc576661dc67b3)
Thanks [@leonidaz](https://github.com/leonidaz)! - A derived created during a
block's run (a `track(() => ...)` or `tracked.readOnly()` in an `@if` branch, a
keyed `@for` item, a render expression, or component setup) is now unsubscribed
from its sources when that block reruns or is destroyed, unless a reader that is
still alive holds it. Before, such deriveds stayed subscribed until the block
that owned them was destroyed, so a branch that toggled repeatedly accumulated
one stale derived per toggle, each walked on every write to the source. The
bookkeeping lives in a side table keyed by the creating block, so blocks that
create no deriveds pay nothing.
- [#1466](https://github.com/Ripple-TS/ripple/pull/1466)
[`2631804`](https://github.com/Ripple-TS/ripple/commit/2631804cd28980b213643ac4ef5e5bcb814eae57)
Thanks [@leonidaz](https://github.com/leonidaz)! - Faster streaming SSR. A
resolved `trackAsync` value made of plain data (strings, finite numbers,
booleans, `null`, arrays and plain objects, each reachable once) now travels in
its hydration script as raw JSON that `JSON.parse` of the envelope yields
directly, instead of a devalue-encoded string; values JSON cannot represent
(`undefined`, `NaN`, `-0`, bigints, Dates, Maps, Sets, shared references,
cycles) keep the devalue encoding, and the client reads either form. Boundaries
that settle in the same task (data arriving together, a batch of promises
resolved by one timer or I/O callback) stream as one chunk, flushed once the
task's microtasks have drained; closing the stream flushes whatever is still
queued first. `devalue` is updated to 5.9.2, with the stringify performance
patch proposed upstream in sveltejs/devalue#190 applied through pnpm's
`patchedDependencies` until it is released.
- [#1472](https://github.com/Ripple-TS/ripple/pull/1472)
[`5d3e60d`](https://github.com/Ripple-TS/ripple/commit/5d3e60d86129ac20d23c86a923914ee44efea9d1)
Thanks [@leonidaz](https://github.com/leonidaz)! - `tracked.readOnly()` returns
a read-only view of a `Tracked` or `WritableDerived`: a `Derived<V>` that
follows the value and rejects writes, for a child, function, or context that
should only read it. It is equivalent to `track(() => tracked.value)`. On a
derived that is already read-only, `readOnly()` returns the derived itself.
- [#1465](https://github.com/Ripple-TS/ripple/pull/1465)
[`8ace206`](https://github.com/Ripple-TS/ripple/commit/8ace206bfed2cd80b758e6e72f94084d2ffe1455)
Thanks [@leonidaz](https://github.com/leonidaz)! - `@try` blocks and the root
boundary keep their state in one explicit `TryState` object, and the pending,
catch, request and streaming helpers are module functions that take it. A
boundary allocates its state and one branch closure instead of a closure per
helper.
- Updated dependencies
[[`bbc1445`](https://github.com/Ripple-TS/ripple/commit/bbc14455485e054c145ebc78f08a771d41ea16f1),
[`0321791`](https://github.com/Ripple-TS/ripple/commit/03217912651e0a49edb8060a92602fd0e0abe193),
[`2ef5da5`](https://github.com/Ripple-TS/ripple/commit/2ef5da52b26fc13e11537dd28df0e2b376ca0f52),
[`0321791`](https://github.com/Ripple-TS/ripple/commit/03217912651e0a49edb8060a92602fd0e0abe193),
[`020d269`](https://github.com/Ripple-TS/ripple/commit/020d2699f8e1333367ac9f7abe9247bab55e272d),
[`453bf4b`](https://github.com/Ripple-TS/ripple/commit/453bf4ba8b1b31963df26696545e446ab4b1658e),
[`411809f`](https://github.com/Ripple-TS/ripple/commit/411809fb38f25b259a1078a901839318e4207871),
[`82bc9ee`](https://github.com/Ripple-TS/ripple/commit/82bc9ee585c6b273bf69890bc2f230aba922bf19),
[`69d50d9`](https://github.com/Ripple-TS/ripple/commit/69d50d9849d8484a88d39b2908bb682d27ec7bc9),
[`0321791`](https://github.com/Ripple-TS/ripple/commit/03217912651e0a49edb8060a92602fd0e0abe193),
[`5d3e60d`](https://github.com/Ripple-TS/ripple/commit/5d3e60d86129ac20d23c86a923914ee44efea9d1)]:
- @tsrx/ripple@0.2.0Related Repositories
affaan-m/ECC★ 285,750
The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.
react/react★ 251,623
The library for web and native user interfaces.
nodejs/node★ 122,515
Node.js JavaScript runtime ✨🐢🚀✨
Leonxlnx/taste-skill★ 100,596
Agent skills that give coding agents taste: art direction, reference images and a browser check instead of generic AI pages
louislam/uptime-kuma★ 92,731
A fancy self-hosted monitoring tool