ilha 0.10.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -285,6 +285,20 @@ interface StateEntry<TInput> {
285
285
  key: string;
286
286
  init: StateInit<TInput, unknown>;
287
287
  }
288
+ /** A state key fed by a generator (`.stream()`). The runtime iterates the
289
+ * generator and writes each yielded value into the state signal. During SSR
290
+ * only the first value is pulled so it can render inline; on the client the
291
+ * generator is consumed continuously. */
292
+ interface StreamEntry<TInput> {
293
+ key: string;
294
+ fn: StreamFn<TInput>;
295
+ }
296
+ type StreamFnContext<TInput> = {
297
+ input: TInput;
298
+ /** Aborts when the island unmounts. Pass through to fetch/SSE layers. */
299
+ signal: AbortSignal;
300
+ };
301
+ type StreamFn<TInput, V = unknown> = (ctx: StreamFnContext<TInput>) => AsyncGenerator<V> | Generator<V>;
288
302
  interface ActionEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
289
303
  key: string;
290
304
  fn: (props: unknown, ctx: ActionContext<TInput, TStateMap, TDerivedMap>) => unknown;
@@ -302,7 +316,7 @@ interface EffectEntry<TInput, TStateMap extends Record<string, unknown>, TDerive
302
316
  /** Where the error originated. `"on"` covers sync throws and async rejections
303
317
  * from `.on()` handlers; `"effect"` covers sync throws from `.effect()` runs
304
318
  * (async work spawned inside an effect is not awaited by the runtime). */
305
- type ErrorSource = "on" | "effect" | "mount" | "transition" | "action";
319
+ type ErrorSource = "on" | "effect" | "mount" | "transition" | "action" | "stream";
306
320
  /**
307
321
  * Register a global error handler invoked when any island reports an error
308
322
  * (from .on, .effect, .onMount, or transitions) and has no local .onError()
@@ -341,6 +355,7 @@ interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDeri
341
355
  /** Shallow defaults merged before props (POJO `.input({ ... })` only). */
342
356
  defaultInput: Record<string, unknown> | null;
343
357
  states: StateEntry<TInput>[];
358
+ streams: StreamEntry<TInput>[];
344
359
  deriveds: DerivedEntry<TInput, TStateMap>[];
345
360
  actions: ActionEntry<TInput, TStateMap, TDerivedMap>[];
346
361
  ons: OnEntry<TInput, TStateMap, TDerivedMap, TActionMap>[];
@@ -360,6 +375,7 @@ declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap exte
360
375
  input<T extends Record<string, unknown>>(defaults: T): IlhaBuilder<T, Record<never, never>, Record<never, never>, Record<never, never>>;
361
376
  as<Tag extends string>(tag: Tag): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
362
377
  state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap, TActionMap>;
378
+ stream<K extends string, V>(key: K, fn: StreamFn<TInput, V>): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap, TActionMap>;
363
379
  derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V>, TActionMap>;
364
380
  action<K extends string, P = undefined, R = void>(key: K, fn: (props: P, ctx: ActionContext<TInput, TStateMap, TDerivedMap>) => R): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap & Record<K, typeof fn>>;
365
381
  on<S extends string>(selectorOrCombined: S, handler: (ctx: S extends `${string}@${infer E}:${string}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap, TActionMap> : S extends `${string}@${infer E}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap, TActionMap> : HandlerContext<TInput, TStateMap, TDerivedMap, TActionMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
@@ -408,6 +424,12 @@ declare function __ilhaJsxSlot({ island, props, key }: {
408
424
  props: Record<string, unknown> | undefined;
409
425
  key: string | undefined;
410
426
  }): RawHtml;
427
+ /**
428
+ * Morph an element's children toward `html`, patching in place: surviving
429
+ * elements keep their identity, listeners, and focus (see the morph engine).
430
+ * Falls back to an innerHTML write when the root tags don't match.
431
+ */
432
+ declare function morph(host: Element, html: string): void;
411
433
  declare const html: typeof ilhaHtml;
412
434
  declare const raw: typeof ilhaRaw;
413
435
  declare const css: typeof ilhaCss;
@@ -420,4 +442,4 @@ declare const context: typeof ilhaContextFn & {
420
442
  clear(): void;
421
443
  };
422
444
  //#endregion
423
- export { __ilhaJsxSlot as A, mount as B, NativeEventModifier as C, SignalSetter as D, SignalAccessor as E, html as F, raw as H, ilha as I, ilhaComputed as L, context as M, css as N, SignalWriter as O, from as P, ilhaEffect as R, NativeEventHandler as S, RawHtml as T, untrack as U, onUncaughtError as V, IslandState as _, EffectContext as a, MountResult as b, ExternalSignal as c, HydratableOptions as d, ISLAND_MOUNT_HANDLES as f, IslandDerived as g, IslandActions as h, DerivedValue as i, batch as j, __ilhaJsxEvent as k, HandlerContext as l, Island as m, ActionContext as n, ErrorContext as o, ISLAND_MOUNT_INTERNAL as p, DerivedAccessor as r, ErrorSource as s, ActionAccessor as t, HandlerContextFor as u, KeyedIsland as v, OnMountContext as w, NativeEventContext as x, MountOptions as y, ilhaSignal as z };
445
+ export { StreamFnContext as A, ilhaEffect as B, NativeEventModifier as C, SignalSetter as D, SignalAccessor as E, css as F, raw as G, morph as H, from as I, untrack as K, html as L, __ilhaJsxSlot as M, batch as N, SignalWriter as O, context as P, ilha as R, NativeEventHandler as S, RawHtml as T, mount as U, ilhaSignal as V, onUncaughtError as W, IslandState as _, EffectContext as a, MountResult as b, ExternalSignal as c, HydratableOptions as d, ISLAND_MOUNT_HANDLES as f, IslandDerived as g, IslandActions as h, DerivedValue as i, __ilhaJsxEvent as j, StreamFn as k, HandlerContext as l, Island as m, ActionContext as n, ErrorContext as o, ISLAND_MOUNT_INTERNAL as p, DerivedAccessor as r, ErrorSource as s, ActionAccessor as t, HandlerContextFor as u, KeyedIsland as v, OnMountContext as w, NativeEventContext as x, MountOptions as y, ilhaComputed as z };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as __ilhaJsxSlot, B as mount, C as NativeEventModifier, D as SignalSetter, E as SignalAccessor, F as html, H as raw, I as ilha, L as ilhaComputed, M as context, N as css, O as SignalWriter, P as from, R as ilhaEffect, S as NativeEventHandler, T as RawHtml, U as untrack, V as onUncaughtError, _ as IslandState, a as EffectContext, b as MountResult, c as ExternalSignal, d as HydratableOptions, f as ISLAND_MOUNT_HANDLES, g as IslandDerived, h as IslandActions, i as DerivedValue, j as batch, k as __ilhaJsxEvent, l as HandlerContext, m as Island, n as ActionContext, o as ErrorContext, p as ISLAND_MOUNT_INTERNAL, r as DerivedAccessor, s as ErrorSource, t as ActionAccessor, u as HandlerContextFor, v as KeyedIsland, w as OnMountContext, x as NativeEventContext, y as MountOptions, z as ilhaSignal } from "./index-BDM797j2.js";
2
- export { ActionAccessor, ActionContext, DerivedAccessor, DerivedValue, EffectContext, ErrorContext, ErrorSource, ExternalSignal, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, Island, IslandActions, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, NativeEventContext, NativeEventHandler, NativeEventModifier, OnMountContext, RawHtml, SignalAccessor, SignalSetter, SignalWriter, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, mount, onUncaughtError, raw, untrack };
1
+ import { A as StreamFnContext, B as ilhaEffect, C as NativeEventModifier, D as SignalSetter, E as SignalAccessor, F as css, G as raw, H as morph, I as from, K as untrack, L as html, M as __ilhaJsxSlot, N as batch, O as SignalWriter, P as context, R as ilha, S as NativeEventHandler, T as RawHtml, U as mount, V as ilhaSignal, W as onUncaughtError, _ as IslandState, a as EffectContext, b as MountResult, c as ExternalSignal, d as HydratableOptions, f as ISLAND_MOUNT_HANDLES, g as IslandDerived, h as IslandActions, i as DerivedValue, j as __ilhaJsxEvent, k as StreamFn, l as HandlerContext, m as Island, n as ActionContext, o as ErrorContext, p as ISLAND_MOUNT_INTERNAL, r as DerivedAccessor, s as ErrorSource, t as ActionAccessor, u as HandlerContextFor, v as KeyedIsland, w as OnMountContext, x as NativeEventContext, y as MountOptions, z as ilhaComputed } from "./index-Cx35Xugv.js";
2
+ export { ActionAccessor, ActionContext, DerivedAccessor, DerivedValue, EffectContext, ErrorContext, ErrorSource, ExternalSignal, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, Island, IslandActions, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, NativeEventContext, NativeEventHandler, NativeEventModifier, OnMountContext, RawHtml, SignalAccessor, SignalSetter, SignalWriter, StreamFn, StreamFnContext, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, morph, mount, onUncaughtError, raw, untrack };
package/dist/index.js CHANGED
@@ -127,13 +127,21 @@ function isStableInlineSlotMount({ initialHtml, renderedHtml, slotIds }) {
127
127
  * map (client) and/or the inlined SSR subtree — never depend on the attr for them.
128
128
  * RawHtml is tagged so a rare attr-only mount can revive `Symbol.for("ilha.raw")`.
129
129
  */
130
- function slotPropsForAttr(props) {
130
+ function slotPropsForAttr(props, captureActions = false) {
131
131
  if (props === void 0) return void 0;
132
132
  let out;
133
133
  for (const key of Object.keys(props)) {
134
134
  if (key === "children") continue;
135
135
  const value = props[key];
136
- if (typeof value === "function" || typeof value === "symbol") continue;
136
+ if (typeof value === "function") {
137
+ const action = captureActions ? captureHandlerActions(value) : void 0;
138
+ if (action) (out ??= {})[key] = {
139
+ __ilha: "action",
140
+ ...action
141
+ };
142
+ continue;
143
+ }
144
+ if (typeof value === "symbol") continue;
137
145
  const encoded = encodeSlotPropValue(value);
138
146
  if (encoded === void 0 && value !== void 0 && value !== null) continue;
139
147
  (out ??= {})[key] = encoded;
@@ -471,11 +479,25 @@ const ASTRO_RENDERER_GLOBAL = Symbol.for("ilha.astroRenderer");
471
479
  * re-renders. Not part of the public surface. */
472
480
  const ISLAND_MOUNT_INTERNAL = Symbol.for("ilha.islandMountInternal");
473
481
  const ISLAND_SLOT_TAG = Symbol.for("ilha.islandSlotTag");
482
+ const ISLAND_CLIENT_REF = Symbol.for("ilha.clientRef");
474
483
  /** @internal Live mount handles keyed by host element. Lets @ilha/router adopt
475
484
  * islands hydrated by `ilha.mount()` (whose handles it never saw) and push new
476
485
  * loader props into them in place instead of remounting. Entries are removed
477
486
  * on unmount. Not part of the public surface. */
478
487
  const ISLAND_MOUNT_HANDLES = /* @__PURE__ */ new WeakMap();
488
+ /** @internal Sync re-render of an island from a state snapshot. Attached by
489
+ * render(); used by server-frame endpoints to produce HTML for streamed
490
+ * updates without a live mount. Not part of the public surface. */
491
+ const ISLAND_RENDER_STATE = Symbol.for("ilha.renderState");
492
+ /** @internal Async SSR render that records the event→action manifest into the
493
+ * provided map. Used by emitIslandSlot so nested server islands ship their
494
+ * interactive surface alongside their inlined HTML. */
495
+ const ISLAND_SSR_MANIFEST = Symbol.for("ilha.ssrManifest");
496
+ function manifestTemplatePrefix(manifest) {
497
+ if (manifest.size === 0) return "";
498
+ const json = escapeHtml(JSON.stringify(Object.fromEntries(manifest)));
499
+ return `<template ${ACTIONS_ATTR}='${json}'></template>`;
500
+ }
479
501
  const SLOT_ATTR = "data-ilha-slot";
480
502
  /** Marks a slot host whose child island is mid leave-transition: it has been
481
503
  * unmounted (and its slot id stripped) but stays connected so the leave
@@ -483,25 +505,63 @@ const SLOT_ATTR = "data-ilha-slot";
483
505
  * or mutating them. */
484
506
  const LEAVING_ATTR = "data-ilha-leaving";
485
507
  const PROPS_ATTR = "data-ilha-props";
508
+ const CLIENT_REF_ATTR = "data-ilha-client-ref";
486
509
  const STATE_ATTR = "data-ilha-state";
487
510
  const CSS_ATTR = "data-ilha-css";
511
+ /** Hydration manifest mapping `${type}:${index}` event-sentinel pairs to the
512
+ * action keys that own them. Emitted by hydratable(); lets server-owned
513
+ * islands (whose render fn never ships to the client) reconnect their
514
+ * interactive surface. */
515
+ const ACTIONS_ATTR = "data-ilha-actions";
488
516
  function buildScopedStyle(css) {
489
517
  const safeCss = css.replace(/<\/style/gi, "<\\/style");
490
518
  return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${safeCss}}</style>`;
491
519
  }
520
+ /** Marks SSR action stubs so event registration never capture-invokes them
521
+ * (invoking the real action body during render would run its side effects). */
522
+ const SSR_ACTION_STUB = Symbol.for("ilha.ssrActionStub");
523
+ const captureStack = [];
524
+ /**
525
+ * Invoke a forwarding closure once against sentinel arguments so any
526
+ * `action.x(...)` call inside it is RECORDED, not executed. Returns the first
527
+ * recorded call, or undefined when the closure throws or calls no action.
528
+ *
529
+ * ponytail: the closure body really executes once per manifest render (SSR
530
+ * and every frame) — only the action call is intercepted. Handlers must be
531
+ * pure "call one action" thunks; side effects before/around the action call
532
+ * run server-side on every frame. Upgrade path: static-scan the closure
533
+ * instead of invoking it.
534
+ */
535
+ function captureHandlerActions(handler) {
536
+ const frame = [];
537
+ captureStack.push(frame);
538
+ try {
539
+ const result = handler({
540
+ preventDefault: () => {},
541
+ stopPropagation: () => {},
542
+ stopImmediatePropagation: () => {},
543
+ persist: () => {}
544
+ }, { signal: new AbortController().signal });
545
+ Promise.resolve(result).catch(() => {});
546
+ } catch {} finally {
547
+ captureStack.pop();
548
+ }
549
+ return frame[0];
550
+ }
492
551
  const RENDER_CTX_STACK = Symbol.for("ilha.renderCtxStack");
493
552
  function renderCtxStack() {
494
553
  const g = globalThis;
495
554
  return g[RENDER_CTX_STACK] ??= [];
496
555
  }
497
- function pushRenderCtx(liveHost, asyncChildren) {
556
+ function pushRenderCtx(liveHost, asyncChildren, manifest) {
498
557
  const ctx = {
499
558
  slots: /* @__PURE__ */ new Map(),
500
559
  positional: 0,
501
560
  liveHost,
502
561
  pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0,
503
562
  binds: [],
504
- events: []
563
+ events: [],
564
+ manifest
505
565
  };
506
566
  renderCtxStack().push(ctx);
507
567
  return ctx;
@@ -531,8 +591,9 @@ function emitIslandSlot({ island, props, key }) {
531
591
  props
532
592
  });
533
593
  const slotTag = getIslandSlotTag(island);
534
- const attrProps = slotPropsForAttr(props);
535
- const propsAttr = attrProps ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(attrProps))}'` : "";
594
+ const clientRef = island[ISLAND_CLIENT_REF];
595
+ const attrProps = slotPropsForAttr(props, typeof clientRef === "string" && ctx?.manifest === true);
596
+ const propsAttr = (attrProps ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(attrProps))}'` : "") + (typeof clientRef === "string" ? ` ${CLIENT_REF_ATTR}="${escapeHtml(clientRef)}"` : "");
536
597
  if (ctx?.liveHost) return wrapIslandSlotHtml({
537
598
  tag: slotTag,
538
599
  id,
@@ -542,9 +603,12 @@ function emitIslandSlot({ island, props, key }) {
542
603
  if (ctx?.pending) {
543
604
  popRenderCtx();
544
605
  try {
545
- const result = island(props);
606
+ const manifest = /* @__PURE__ */ new Map();
607
+ const ssrWithManifest = island[ISLAND_SSR_MANIFEST];
608
+ const result = ssrWithManifest ? ssrWithManifest(props, manifest) : island(props);
609
+ const prefix = () => manifestTemplatePrefix(manifest);
546
610
  if (result instanceof Promise) {
547
- ctx.pending.set(id, result.then(String));
611
+ ctx.pending.set(id, result.then(String).then((html) => prefix() + html));
548
612
  return wrapIslandSlotHtml({
549
613
  tag: slotTag,
550
614
  id,
@@ -556,7 +620,7 @@ function emitIslandSlot({ island, props, key }) {
556
620
  tag: slotTag,
557
621
  id,
558
622
  propsAttr,
559
- inner: String(result)
623
+ inner: prefix() + String(result)
560
624
  });
561
625
  } finally {
562
626
  renderCtxStack().push(ctx);
@@ -853,6 +917,8 @@ function ilhaHtml(strings, ...values) {
853
917
  const modifier = NATIVE_EVENT_MODIFIERS.has(rawModifier) ? rawModifier : void 0;
854
918
  const openQuote = eventMatch[3] ?? null;
855
919
  const cleanPrefix = chunk.slice(0, eventMatchStart).replace(/\s+$/, "");
920
+ const isActionStub = typeof value === "function" && SSR_ACTION_STUB in value;
921
+ const capture = ctx?.manifest && typeof value === "function" && !isActionStub ? captureHandlerActions(value) : void 0;
856
922
  result += cleanPrefix;
857
923
  if (ctx && typeof value === "function") {
858
924
  const index = ctx.events.length;
@@ -860,7 +926,8 @@ function ilhaHtml(strings, ...values) {
860
926
  ctx.events.push({
861
927
  type: eventName,
862
928
  handler: value,
863
- modifier
929
+ modifier,
930
+ capture
864
931
  });
865
932
  pendingEventSpecs += (pendingEventSpecs ? "," : "") + `${eventName}:${index}`;
866
933
  }
@@ -1629,6 +1696,7 @@ var IlhaBuilder = class IlhaBuilder {
1629
1696
  schema,
1630
1697
  defaultInput,
1631
1698
  states: [],
1699
+ streams: [],
1632
1700
  deriveds: [],
1633
1701
  actions: [],
1634
1702
  ons: [],
@@ -1656,6 +1724,21 @@ var IlhaBuilder = class IlhaBuilder {
1656
1724
  }]
1657
1725
  });
1658
1726
  }
1727
+ stream(key, fn) {
1728
+ const cfg = this._cfg;
1729
+ const states = cfg.states.some((entry) => entry.key === key) ? cfg.states : [...cfg.states, {
1730
+ key,
1731
+ init: void 0
1732
+ }];
1733
+ return new IlhaBuilder({
1734
+ ...cfg,
1735
+ states,
1736
+ streams: [...cfg.streams.filter((entry) => entry.key !== key), {
1737
+ key,
1738
+ fn
1739
+ }]
1740
+ });
1741
+ }
1659
1742
  derived(key, fn) {
1660
1743
  const cfg = this._cfg;
1661
1744
  return new IlhaBuilder({
@@ -1729,7 +1812,7 @@ var IlhaBuilder = class IlhaBuilder {
1729
1812
  });
1730
1813
  }
1731
1814
  render(fn) {
1732
- const { schema, defaultInput, states, deriveds, actions, ons, effects, onMounts, onErrors, transition, css: cssSource, as: slotAs } = this._cfg;
1815
+ const { schema, defaultInput, states, streams, deriveds, actions, ons, effects, onMounts, onErrors, transition, css: cssSource, as: slotAs } = this._cfg;
1733
1816
  const stylePrefix = cssSource == null ? "" : buildScopedStyle(cssSource);
1734
1817
  const configuredSlotTag = slotAs ?? "div";
1735
1818
  function resolveInput(props) {
@@ -1740,8 +1823,8 @@ var IlhaBuilder = class IlhaBuilder {
1740
1823
  if (!schema) return merged;
1741
1824
  return validateSchema(schema, merged);
1742
1825
  }
1743
- function renderWithCtx({ render, liveHost, asyncChildren }) {
1744
- const ctx = pushRenderCtx(liveHost, asyncChildren);
1826
+ function renderWithCtx({ render, liveHost, asyncChildren, manifest }) {
1827
+ const ctx = pushRenderCtx(liveHost, asyncChildren, manifest);
1745
1828
  try {
1746
1829
  const html = unwrapHtml(render());
1747
1830
  const slots = ctx.slots;
@@ -1794,8 +1877,19 @@ var IlhaBuilder = class IlhaBuilder {
1794
1877
  }
1795
1878
  function buildSsrActions() {
1796
1879
  const out = {};
1880
+ const lookup = /* @__PURE__ */ new Map();
1797
1881
  for (const entry of actions) {
1798
- const invoke = () => {};
1882
+ const invoke = (...callArgs) => {
1883
+ const frame = captureStack[captureStack.length - 1];
1884
+ if (frame) {
1885
+ if (!frame.some((r) => r.k === entry.key)) frame.push({
1886
+ k: entry.key,
1887
+ a: callArgs
1888
+ });
1889
+ return;
1890
+ }
1891
+ };
1892
+ invoke[SSR_ACTION_STUB] = true;
1799
1893
  Object.defineProperties(invoke, {
1800
1894
  pending: {
1801
1895
  get: () => false,
@@ -1811,8 +1905,12 @@ var IlhaBuilder = class IlhaBuilder {
1811
1905
  }
1812
1906
  });
1813
1907
  out[entry.key] = invoke;
1908
+ lookup.set(invoke, entry.key);
1814
1909
  }
1815
- return out;
1910
+ return {
1911
+ action: out,
1912
+ lookup
1913
+ };
1816
1914
  }
1817
1915
  /** Detached host for SSR setup hooks; must not be `{}` (Areia/components call host.matches). */
1818
1916
  function createSsrOnMountHost() {
@@ -1880,11 +1978,45 @@ var IlhaBuilder = class IlhaBuilder {
1880
1978
  }
1881
1979
  }
1882
1980
  }
1883
- function renderToString(props, sync = false) {
1981
+ function renderToString(props, sync = false, streamOut, eventsOut, stateOverride, forceAsyncChildren = false) {
1884
1982
  const input = resolveInput(props);
1885
- const state = buildSignalState(input);
1886
- const action = buildSsrActions();
1887
- const results = deriveds.map((entry) => {
1983
+ const state = buildSignalState(input, stateOverride);
1984
+ const { action, lookup: actionLookup } = buildSsrActions();
1985
+ const recordEventsManifest = (events) => {
1986
+ if (!eventsOut || events.length === 0) return;
1987
+ events.forEach((record, index) => {
1988
+ if (record.capture) {
1989
+ eventsOut.set(`${record.type}:${index}`, record.capture);
1990
+ return;
1991
+ }
1992
+ const key = actionLookup.get(record.handler);
1993
+ if (key) eventsOut.set(`${record.type}:${index}`, key);
1994
+ });
1995
+ };
1996
+ const pullStreamInitials = async (streamOut) => {
1997
+ for (const entry of streams) {
1998
+ const controller = new AbortController();
1999
+ try {
2000
+ const gen = await entry.fn({
2001
+ input,
2002
+ signal: controller.signal
2003
+ });
2004
+ const first = await gen.next();
2005
+ controller.abort();
2006
+ await gen.return?.(void 0);
2007
+ if (first.done) continue;
2008
+ if (streamOut) streamOut[entry.key] = first.value;
2009
+ const prevSub = setActiveSub(void 0);
2010
+ try {
2011
+ const setter = state[entry.key];
2012
+ setter?.(first.value);
2013
+ } finally {
2014
+ setActiveSub(prevSub);
2015
+ }
2016
+ } catch {}
2017
+ }
2018
+ };
2019
+ const computeDerivedResults = () => deriveds.map((entry) => {
1888
2020
  const prevSub = setActiveSub(void 0);
1889
2021
  try {
1890
2022
  return {
@@ -1904,84 +2036,95 @@ var IlhaBuilder = class IlhaBuilder {
1904
2036
  setActiveSub(prevSub);
1905
2037
  }
1906
2038
  });
1907
- if (!results.some((r) => r.result instanceof Promise) || sync) {
1908
- const envelopes = {};
1909
- for (const r of results) if (r.result instanceof Promise) envelopes[r.key] = {
1910
- loading: true,
1911
- value: void 0,
1912
- error: void 0
1913
- };
1914
- else envelopes[r.key] = {
1915
- loading: false,
1916
- value: r.result,
1917
- error: void 0
1918
- };
1919
- const derived = buildDerivedAccessors(envelopes);
1920
- if (!currentRenderCtx()) runOnMountForRender({
1921
- input,
1922
- state,
1923
- derived,
1924
- action
1925
- });
1926
- const prevSub = setActiveSub(void 0);
1927
- try {
1928
- const { html } = renderWithCtx({ render: () => fn({
1929
- state,
1930
- derived,
1931
- action,
1932
- input
1933
- }) });
1934
- return stylePrefix + html;
1935
- } finally {
1936
- setActiveSub(prevSub);
1937
- }
1938
- }
1939
- return Promise.all(results.map(async (r) => {
1940
- try {
1941
- return {
1942
- key: r.key,
1943
- envelope: {
1944
- loading: false,
1945
- value: await Promise.resolve(r.result),
1946
- error: void 0
1947
- }
2039
+ if (!sync && (streams.length > 0 || forceAsyncChildren)) return pullStreamInitials(streamOut).then(() => renderToStringTail());
2040
+ return renderToStringTail();
2041
+ function renderToStringTail() {
2042
+ const results = computeDerivedResults();
2043
+ if (!(forceAsyncChildren || results.some((r) => r.result instanceof Promise)) || sync) {
2044
+ const envelopes = {};
2045
+ for (const r of results) if (r.result instanceof Promise) envelopes[r.key] = {
2046
+ loading: true,
2047
+ value: void 0,
2048
+ error: void 0
1948
2049
  };
1949
- } catch (err) {
1950
- return {
1951
- key: r.key,
1952
- envelope: {
1953
- loading: false,
1954
- value: void 0,
1955
- error: err instanceof Error ? err : new Error(String(err))
1956
- }
2050
+ else envelopes[r.key] = {
2051
+ loading: false,
2052
+ value: r.result,
2053
+ error: void 0
1957
2054
  };
1958
- }
1959
- })).then(async (resolved) => {
1960
- const envelopes = {};
1961
- for (const r of resolved) envelopes[r.key] = r.envelope;
1962
- const derived = buildDerivedAccessors(envelopes);
1963
- if (!currentRenderCtx()) runOnMountForRender({
1964
- input,
1965
- state,
1966
- derived,
1967
- action
1968
- });
1969
- const prevSub = setActiveSub(void 0);
1970
- try {
1971
- const { html } = await renderWithCtx({
1972
- render: () => fn({
1973
- state,
1974
- derived,
1975
- action,
1976
- input
1977
- }),
1978
- asyncChildren: true
2055
+ const derived = buildDerivedAccessors(envelopes);
2056
+ if (!currentRenderCtx()) runOnMountForRender({
2057
+ input,
2058
+ state,
2059
+ derived,
2060
+ action
1979
2061
  });
1980
- return stylePrefix + html;
1981
- } finally {
1982
- setActiveSub(prevSub);
2062
+ const prevSub = setActiveSub(void 0);
2063
+ try {
2064
+ const { html, events } = renderWithCtx({
2065
+ render: () => fn({
2066
+ state,
2067
+ derived,
2068
+ action,
2069
+ input
2070
+ }),
2071
+ manifest: eventsOut !== void 0
2072
+ });
2073
+ recordEventsManifest(events);
2074
+ return stylePrefix + html;
2075
+ } finally {
2076
+ setActiveSub(prevSub);
2077
+ }
1983
2078
  }
1984
- });
2079
+ return Promise.all(results.map(async (r) => {
2080
+ try {
2081
+ return {
2082
+ key: r.key,
2083
+ envelope: {
2084
+ loading: false,
2085
+ value: await Promise.resolve(r.result),
2086
+ error: void 0
2087
+ }
2088
+ };
2089
+ } catch (err) {
2090
+ return {
2091
+ key: r.key,
2092
+ envelope: {
2093
+ loading: false,
2094
+ value: void 0,
2095
+ error: err instanceof Error ? err : new Error(String(err))
2096
+ }
2097
+ };
2098
+ }
2099
+ })).then(async (resolved) => {
2100
+ const envelopes = {};
2101
+ for (const r of resolved) envelopes[r.key] = r.envelope;
2102
+ const derived = buildDerivedAccessors(envelopes);
2103
+ if (!currentRenderCtx()) runOnMountForRender({
2104
+ input,
2105
+ state,
2106
+ derived,
2107
+ action
2108
+ });
2109
+ const prevSub = setActiveSub(void 0);
2110
+ try {
2111
+ const { html, events } = await renderWithCtx({
2112
+ render: () => fn({
2113
+ state,
2114
+ derived,
2115
+ action,
2116
+ input
2117
+ }),
2118
+ asyncChildren: true,
2119
+ manifest: eventsOut !== void 0
2120
+ });
2121
+ recordEventsManifest(events);
2122
+ return stylePrefix + html;
2123
+ } finally {
2124
+ setActiveSub(prevSub);
2125
+ }
2126
+ });
2127
+ }
1985
2128
  }
1986
2129
  function mountIsland(host, props) {
1987
2130
  return mountIslandInternal(host, props).unmount;
@@ -2025,7 +2168,7 @@ var IlhaBuilder = class IlhaBuilder {
2025
2168
  const parsed = safeParseSnapshot(rawState, STATE_ATTR);
2026
2169
  if (parsed !== void 0) snapshotRaw = parsed;
2027
2170
  }
2028
- const stateSnapshot = snapshotRaw ? Object.fromEntries(Object.entries(snapshotRaw).filter(([k]) => k !== "_derived" && k !== "_skipOnMount")) : void 0;
2171
+ const stateSnapshot = snapshotRaw ? Object.fromEntries(Object.entries(snapshotRaw).filter(([k]) => k !== "_derived" && k !== "_skipOnMount" && k !== "_streams")) : void 0;
2029
2172
  const derivedSnapshotRaw = snapshotRaw?._derived;
2030
2173
  let derivedSnapshot;
2031
2174
  if (derivedSnapshotRaw) {
@@ -2454,6 +2597,36 @@ var IlhaBuilder = class IlhaBuilder {
2454
2597
  }
2455
2598
  stopDerived = setupDerived();
2456
2599
  cleanups.push(stopDerived);
2600
+ for (const entry of streams) {
2601
+ let streamAborted = false;
2602
+ const streamController = new AbortController();
2603
+ const runStream = async () => {
2604
+ try {
2605
+ const gen = await entry.fn({
2606
+ input,
2607
+ signal: streamController.signal
2608
+ });
2609
+ while (!streamAborted && !streamController.signal.aborted) {
2610
+ const { done, value } = await gen.next();
2611
+ if (streamAborted || streamController.signal.aborted || done) break;
2612
+ const prevSub = setActiveSub(void 0);
2613
+ try {
2614
+ const setter = state[entry.key];
2615
+ setter?.(value);
2616
+ } finally {
2617
+ setActiveSub(prevSub);
2618
+ }
2619
+ }
2620
+ } catch (err) {
2621
+ if (!streamAborted && !streamController.signal.aborted && err?.name !== "AbortError") reportError(err, "stream");
2622
+ }
2623
+ };
2624
+ runStream();
2625
+ cleanups.push(() => {
2626
+ streamAborted = true;
2627
+ streamController.abort();
2628
+ });
2629
+ }
2457
2630
  let initialized = false;
2458
2631
  const initialRenderedHtml = initial.html;
2459
2632
  let lastRendered = null;
@@ -2633,6 +2806,12 @@ var IlhaBuilder = class IlhaBuilder {
2633
2806
  island.mount = (host, props) => mountIsland(host, props);
2634
2807
  island[ISLAND_MOUNT_INTERNAL] = (host, props) => mountIslandInternal(host, props);
2635
2808
  island[ISLAND_SLOT_TAG] = configuredSlotTag;
2809
+ island[ISLAND_RENDER_STATE] = async (props) => {
2810
+ const manifest = /* @__PURE__ */ new Map();
2811
+ const html = await renderToString(props ?? void 0, false, void 0, manifest, void 0, true);
2812
+ return manifestTemplatePrefix(manifest) + html;
2813
+ };
2814
+ island[ISLAND_SSR_MANIFEST] = (props, eventsOut) => renderToString(props, false, void 0, eventsOut, void 0, true);
2636
2815
  island.key = (key) => {
2637
2816
  if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
2638
2817
  if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
@@ -2732,7 +2911,9 @@ var IlhaBuilder = class IlhaBuilder {
2732
2911
  const { name, as: rawTag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
2733
2912
  const tag = assertValidSlotTagName(rawTag);
2734
2913
  const resolvedProps = props ?? {};
2735
- const inner = await renderToString(resolvedProps);
2914
+ const streamValues = {};
2915
+ const eventsManifest = /* @__PURE__ */ new Map();
2916
+ const inner = await renderToString(resolvedProps, false, streamValues, eventsManifest, void 0, true);
2736
2917
  const encodedProps = escapeHtml(JSON.stringify(resolvedProps));
2737
2918
  let stateAttr = "";
2738
2919
  if (snapshot !== false) {
@@ -2742,7 +2923,9 @@ var IlhaBuilder = class IlhaBuilder {
2742
2923
  const snapshotData = {};
2743
2924
  const input = resolveInput(resolvedProps);
2744
2925
  const plainState = buildPlainState(input);
2745
- if (doState) for (const entry of states) snapshotData[entry.key] = plainState[entry.key]();
2926
+ const stateForDerived = { ...plainState };
2927
+ for (const [k, v] of Object.entries(streamValues)) stateForDerived[k] = () => v;
2928
+ if (doState) for (const entry of states) snapshotData[entry.key] = entry.key in streamValues ? streamValues[entry.key] : plainState[entry.key]();
2746
2929
  if (doDerived) {
2747
2930
  const derivedResults = {};
2748
2931
  for (const entry of deriveds) {
@@ -2752,7 +2935,7 @@ var IlhaBuilder = class IlhaBuilder {
2752
2935
  let threw = false;
2753
2936
  try {
2754
2937
  resultPromise = entry.fn({
2755
- state: plainState,
2938
+ state: stateForDerived,
2756
2939
  input,
2757
2940
  signal: new AbortController().signal
2758
2941
  });
@@ -2788,9 +2971,12 @@ var IlhaBuilder = class IlhaBuilder {
2788
2971
  snapshotData["_derived"] = derivedResults;
2789
2972
  }
2790
2973
  if (doSkipOnMount) snapshotData["_skipOnMount"] = true;
2974
+ if (streams.length > 0) snapshotData["_streams"] = Object.fromEntries(streams.map((entry) => [entry.key, true]));
2791
2975
  stateAttr = ` ${STATE_ATTR}='${escapeHtml(JSON.stringify(snapshotData))}'`;
2792
2976
  }
2793
- return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${encodedProps}'${stateAttr}>${inner}</${tag}>`;
2977
+ let actionsAttr = "";
2978
+ if (eventsManifest.size > 0) actionsAttr = ` ${ACTIONS_ATTR}='${escapeHtml(JSON.stringify(Object.fromEntries(eventsManifest)))}'`;
2979
+ return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${encodedProps}'${stateAttr}${actionsAttr}>${inner}</${tag}>`;
2794
2980
  };
2795
2981
  return island;
2796
2982
  }
@@ -2852,6 +3038,7 @@ const rootBuilder = new IlhaBuilder({
2852
3038
  schema: null,
2853
3039
  defaultInput: null,
2854
3040
  states: [],
3041
+ streams: [],
2855
3042
  deriveds: [],
2856
3043
  actions: [],
2857
3044
  ons: [],
@@ -2888,11 +3075,14 @@ const ilha = Object.assign(callableRoot, {
2888
3075
  function __ilhaJsxEvent({ type, handler, modifier }) {
2889
3076
  const ctx = currentRenderCtx();
2890
3077
  if (!ctx) return void 0;
3078
+ const isActionStub = SSR_ACTION_STUB in handler;
3079
+ const capture = ctx.manifest && !isActionStub ? captureHandlerActions(handler) : void 0;
2891
3080
  const index = ctx.events.length;
2892
3081
  ctx.events.push({
2893
3082
  type,
2894
3083
  handler,
2895
- modifier
3084
+ modifier,
3085
+ capture
2896
3086
  });
2897
3087
  return index;
2898
3088
  }
@@ -2904,6 +3094,27 @@ function __ilhaJsxSlot({ island, props, key }) {
2904
3094
  key
2905
3095
  }));
2906
3096
  }
3097
+ /**
3098
+ * Morph an element's children toward `html`, patching in place: surviving
3099
+ * elements keep their identity, listeners, and focus (see the morph engine).
3100
+ * Falls back to an innerHTML write when the root tags don't match.
3101
+ */
3102
+ function morph(host, html) {
3103
+ if (typeof document === "undefined") return;
3104
+ const openTag = host.localName ?? "div";
3105
+ const tpl = document.createElement("template");
3106
+ tpl.innerHTML = `<${openTag}>${html}</${openTag}>`;
3107
+ const next = tpl.content.firstElementChild;
3108
+ if (!next || next.localName !== host.localName) {
3109
+ host.innerHTML = html;
3110
+ return;
3111
+ }
3112
+ try {
3113
+ morphInner(host, next);
3114
+ } catch {
3115
+ host.innerHTML = html;
3116
+ }
3117
+ }
2907
3118
  const html = ilhaHtml;
2908
3119
  const raw = ilhaRaw;
2909
3120
  const css = ilhaCss;
@@ -2912,4 +3123,4 @@ const from = ilhaFrom;
2912
3123
  const context = ilhaContext;
2913
3124
 
2914
3125
  //#endregion
2915
- export { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, mount, onUncaughtError, raw, untrack };
3126
+ export { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, morph, mount, onUncaughtError, raw, untrack };
@@ -1,2 +1,2 @@
1
- import { a as JSX, r as jsxDEV, t as Fragment } from "./jsx-runtime-C6R6IiOD.js";
1
+ import { a as JSX, r as jsxDEV, t as Fragment } from "./jsx-runtime-B-viVOB1.js";
2
2
  export { Fragment, type JSX, jsxDEV };
@@ -1,4 +1,4 @@
1
- import { C as NativeEventModifier, E as SignalAccessor, O as SignalWriter, S as NativeEventHandler, T as RawHtml } from "./index-BDM797j2.js";
1
+ import { C as NativeEventModifier, E as SignalAccessor, O as SignalWriter, S as NativeEventHandler, T as RawHtml } from "./index-Cx35Xugv.js";
2
2
  //#region src/jsx-types.d.ts
3
3
  declare namespace JSX {
4
4
  export type Child = unknown;
@@ -1,2 +1,2 @@
1
- import { a as JSX, i as jsxs, n as jsx, r as jsxDEV, t as Fragment } from "./jsx-runtime-C6R6IiOD.js";
1
+ import { a as JSX, i as jsxs, n as jsx, r as jsxDEV, t as Fragment } from "./jsx-runtime-B-viVOB1.js";
2
2
  export { Fragment, type JSX, jsx, jsxDEV, jsxs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "description": "A tiny, framework-free island architecture library",
5
5
  "keywords": [
6
6
  "framework-free",