ilha 0.14.0 → 0.14.2

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.
package/README.md CHANGED
@@ -48,43 +48,48 @@ count.set(1); // replace
48
48
  count.update((n) => n + 1); // patch
49
49
  ```
50
50
 
51
- In JSX, `{count}` subscribes the render. A function initializer is a **computed** atom:
51
+ In JSX, `{count}` subscribes the render. Derived values use Effect's `Atom.map` or `Atom.transform`:
52
52
 
53
53
  ```tsx
54
+ import * as Atom from "effect/unstable/reactivity/Atom";
55
+ import { atom } from "ilha";
56
+
54
57
  const items = atom([{ n: 1 }, { n: 2 }]);
55
- const total = atom(() => items().reduce((sum, item) => sum + item.n, 0));
58
+ const total = atom(Atom.map(items.atom, (list) => list.reduce((sum, item) => sum + item.n, 0)));
56
59
  ```
57
60
 
61
+ Wrap multiple writes in `batch()`. Derived and mutation atoms use Effect's `Atom.map`, `Atom.transform`, and `Atom.fn` — pass `handle.atom`, then wrap in `atom()`. Use `watch(source, fn)` for side effects on atom changes.
62
+
63
+ Use `atom.lazy(() => …)` for one-time initialization or to store a function value.
64
+
58
65
  Atoms hold data. Do not store JSX in an atom.
59
66
 
60
67
  ---
61
68
 
62
- ## Streams, when, watch, wait
69
+ ## Streams and when
63
70
 
64
- Use a generator when you `yield*` instructions.
71
+ Paint [Effect `Stream`](https://www.effect.website/docs/v4/api/effect/Stream) values. Yield a stream of views from a generator, or return one from an async component:
65
72
 
66
73
  ```tsx
67
74
  import * as Stream from "effect/Stream";
68
75
  import * as Atom from "effect/unstable/reactivity/Atom";
69
- import { atom, when } from "ilha";
76
+ import { atom } from "ilha";
70
77
 
71
- const Search = function* () {
72
- const q = atom("");
73
- yield (
74
- <input value={q} oninput={(e: Event) => q.set((e.currentTarget as HTMLInputElement).value)} />
75
- );
76
- yield* when(Atom.toStream(q.atom).pipe(Stream.debounce("200 millis")), function* (query) {
77
- if (!query) return;
78
- yield <p>{query}</p>;
79
- });
80
- };
78
+ function* List() {
79
+ const items = atom(["a", "b"]);
80
+ yield Stream.map(Atom.toStream(items.atom), (list) => (
81
+ <ul>
82
+ {list.map((item) => (
83
+ <li key={item}>{item}</li>
84
+ ))}
85
+ </ul>
86
+ ));
87
+ }
81
88
  ```
82
89
 
83
- - `when(stream, body)` — render `body` for each value (SSR takes the first).
84
- - `watch(source, fn)` — side effect on an atom or Stream.
85
- - `wait(body)` — paint until `done(value)`, then continue the generator.
90
+ SSR takes the first emission (`take(1)`); the client keeps listening after `mount`.
86
91
 
87
- Map a Stream of arrays to JSX for lists (`Stream.map`, `Atom.toStream`).
92
+ Ilha also ships `when` for per-emission generator bodies (stale work interrupted). Side effects and pauses use Effect directly — see the [Streams guide](https://ilha.build/guide/ui/streams).
88
93
 
89
94
  ---
90
95
 
@@ -112,7 +117,7 @@ if (host) mount(host, Counter, { hydrate: true });
112
117
  | `timeout` | none | Serialize after this many ms |
113
118
  | `captureActions` | `false` | Probe handlers for server RPCs |
114
119
 
115
- In Node, `renderToString` registers happy-dom when `document` is missing.
120
+ `renderToString` builds HTML without a DOM — no polyfill in Node, Bun, or edge runtimes.
116
121
 
117
122
  ---
118
123
 
package/dist/define.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as Component } from "./types-DU46exHq.js";
1
+ import { n as Component } from "./types-Cie0Ihl6.js";
2
2
  //#region src/define.d.ts
3
3
  declare function define(name: string, component: Component): void;
4
4
  //#endregion
package/dist/define.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as mount } from "./mount-Dcy4teaf.js";
1
+ import { t as mount } from "./mount-CCMOkDSb.js";
2
2
 
3
3
  //#region src/define.ts
4
4
  function define(name, component) {
package/dist/index.d.ts CHANGED
@@ -1,23 +1,25 @@
1
- import { a as Instruction, c as Yielded, i as Fragment, n as Component, r as Done, s as View, t as AtomHandle } from "./types-DU46exHq.js";
2
- import { t as h } from "./vnode-BZLcG8lC.js";
1
+ import { i as Instruction, n as Component, o as View, r as Fragment, s as Yielded, t as AtomHandle } from "./types-Cie0Ihl6.js";
2
+ import { t as h } from "./vnode-DyFwtMVw.js";
3
3
  import * as Effect from "effect/Effect";
4
4
  import * as Stream from "effect/Stream";
5
5
  import * as Atom from "effect/unstable/reactivity/Atom";
6
6
  import "effect/Scope";
7
7
  import { AtomRegistry } from "effect/unstable/reactivity/AtomRegistry";
8
+ //#region src/watch.d.ts
9
+ /** Run `fn` when `source` changes — and once on mount. Sync, async, and generator components. */
10
+ declare function watch<A>(source: AtomHandle<A> | Atom.Atom<A> | Stream.Stream<A, any, any>, fn: (value: A) => void): Instruction<void>;
11
+ //#endregion
8
12
  //#region src/atom.d.ts
9
- declare function atom<A>(init: () => A): AtomHandle<A>;
10
- declare function atom<A>(init: A | Atom.Atom<A> | Effect.Effect<A, any, any> | Stream.Stream<A, any, any>): AtomHandle<A>;
13
+ interface AtomFn {
14
+ <A>(init: A | Atom.Atom<A> | Effect.Effect<A, any, any> | Stream.Stream<A, any, any>): AtomHandle<A>;
15
+ lazy<A>(init: () => A): AtomHandle<A>;
16
+ }
17
+ declare const atom: AtomFn;
18
+ declare const batch: (f: () => void) => void;
11
19
  //#endregion
12
20
  //#region src/when.d.ts
13
21
  declare function when<A, E = never, R = never>(stream: Stream.Stream<A, E, R>, body: (value: A) => Generator<Yielded, View | void, unknown>): Instruction<void, E>;
14
22
  //#endregion
15
- //#region src/watch.d.ts
16
- declare function watch<A>(source: AtomHandle<A> | Atom.Atom<A> | Stream.Stream<A, any, any>, fn: (value: A) => void): Instruction<void>;
17
- //#endregion
18
- //#region src/wait.d.ts
19
- declare function wait<A>(body: (done: Done<A>) => Generator<Yielded, View | void, unknown>): Instruction<A>;
20
- //#endregion
21
23
  //#region src/mount.d.ts
22
24
  type RenderToStringOptions = {
23
25
  snapshot?: boolean;
@@ -32,4 +34,4 @@ type MountOptions = {
32
34
  declare function mount(el: Element, fn: Component, opts?: MountOptions): () => void;
33
35
  declare function renderToString(fn: Component, opts?: RenderToStringOptions): Promise<string>;
34
36
  //#endregion
35
- export { type AtomHandle, type Component, type Done, Fragment, type MountOptions, type RenderToStringOptions, type View, atom, h, mount, renderToString, wait, watch, when };
37
+ export { type AtomHandle, type Component, Fragment, type MountOptions, type RenderToStringOptions, type View, atom, batch, h, mount, renderToString, watch, when };
package/dist/index.js CHANGED
@@ -1,10 +1,69 @@
1
- import { a as atom, c as isAtomHandle, d as makeFiber, i as interpret, l as closeFiber, n as renderToString, o as handleOwner, r as paint, s as instr, t as mount, u as getFiber } from "./mount-Dcy4teaf.js";
1
+ import { a as atom, c as instr, d as getFiber, f as makeFiber, i as interpret, l as isAtomHandle, n as renderToString, o as batch, r as paint, s as handleOwner, t as mount, u as closeFiber } from "./mount-CCMOkDSb.js";
2
2
  import { i as Fragment, t as h } from "./vnode-DHN2EFHi.js";
3
3
  import * as Effect from "effect/Effect";
4
4
  import * as Stream from "effect/Stream";
5
+ import * as Exit from "effect/Exit";
6
+ import * as Fiber from "effect/Fiber";
5
7
  import * as Scope from "effect/Scope";
6
- import * as Deferred from "effect/Deferred";
8
+ import * as Registry from "effect/unstable/reactivity/AtomRegistry";
7
9
 
10
+ //#region src/watch.ts
11
+ function runStreamWatch(fiber, effect) {
12
+ const scope = Scope.forkUnsafe(fiber.scope);
13
+ const provided = effect.pipe(Effect.provideService(Registry.AtomRegistry, fiber.registry));
14
+ const fork = Effect.runFork(provided);
15
+ Effect.runSync(Scope.addFinalizer(scope, Fiber.interrupt(fork)));
16
+ return () => {
17
+ Effect.runFork(Scope.close(scope, Exit.void));
18
+ };
19
+ }
20
+ function useWatchSlot(fiber, key, mount, fn) {
21
+ const i = fiber.watchI ?? 0;
22
+ fiber.watchI = i + 1;
23
+ fiber.watchSlots ??= [];
24
+ const existing = fiber.watchSlots[i];
25
+ if (existing) {
26
+ if (existing.key !== key) {
27
+ existing.dispose();
28
+ fiber.watchSlots[i] = void 0;
29
+ } else {
30
+ existing.fnRef.current = fn;
31
+ return;
32
+ }
33
+ }
34
+ const fnRef = { current: fn };
35
+ const unsub = mount((value) => fnRef.current(value));
36
+ const dispose = () => unsub();
37
+ fiber.watchSlots[i] = {
38
+ key,
39
+ fnRef,
40
+ dispose
41
+ };
42
+ }
43
+ function registerWatch(fiber, source, fn) {
44
+ if (Stream.isStream(source)) {
45
+ useWatchSlot(fiber, source, (run) => {
46
+ let active = true;
47
+ const stop = runStreamWatch(fiber, Stream.runForEach(source, (v) => Effect.sync(() => {
48
+ if (active) run(v);
49
+ })));
50
+ return () => {
51
+ active = false;
52
+ stop();
53
+ };
54
+ }, fn);
55
+ return;
56
+ }
57
+ const atom = isAtomHandle(source) ? source.atom : source;
58
+ useWatchSlot(fiber, atom, (run) => fiber.registry.subscribe(atom, run, { immediate: true }), fn);
59
+ }
60
+ /** Run `fn` when `source` changes — and once on mount. Sync, async, and generator components. */
61
+ function watch(source, fn) {
62
+ registerWatch((isAtomHandle(source) ? handleOwner.get(source) : void 0) ?? getFiber(), source, fn);
63
+ return instr(Effect.void);
64
+ }
65
+
66
+ //#endregion
8
67
  //#region src/when.ts
9
68
  function when(stream, body) {
10
69
  return instr(Effect.gen(function* () {
@@ -25,41 +84,4 @@ function when(stream, body) {
25
84
  }
26
85
 
27
86
  //#endregion
28
- //#region src/watch.ts
29
- function watch(source, fn) {
30
- return instr(Effect.sync(() => {
31
- const fiber = (isAtomHandle(source) ? handleOwner.get(source) : void 0) ?? getFiber();
32
- if (Stream.isStream(source)) {
33
- fiber.run(Stream.runForEach(source, (v) => Effect.sync(() => fn(v))));
34
- return;
35
- }
36
- const a = isAtomHandle(source) ? source.atom : source;
37
- const unsub = fiber.registry.subscribe(a, fn, { immediate: true });
38
- Effect.runSync(Scope.addFinalizer(fiber.scope, Effect.sync(unsub)));
39
- }));
40
- }
41
-
42
- //#endregion
43
- //#region src/wait.ts
44
- function wait(body) {
45
- return instr(Effect.gen(function* () {
46
- const parent = getFiber();
47
- const deferred = yield* Deferred.make();
48
- let settled = false;
49
- const done = (value) => {
50
- if (settled) return;
51
- settled = true;
52
- Effect.runSync(Deferred.succeed(deferred, value));
53
- };
54
- const child = makeFiber(parent.runtime, parent.root, paint, { onFail: parent.fail });
55
- interpret((() => body(done)), child);
56
- try {
57
- return yield* Deferred.await(deferred);
58
- } finally {
59
- closeFiber(child);
60
- }
61
- }));
62
- }
63
-
64
- //#endregion
65
- export { Fragment, atom, h, mount, renderToString, wait, watch, when };
87
+ export { Fragment, atom, batch, h, mount, renderToString, watch, when };
@@ -1,4 +1,4 @@
1
- import { i as Fragment } from "./types-DU46exHq.js";
2
- import { n as JSX } from "./vnode-BZLcG8lC.js";
1
+ import { r as Fragment } from "./types-Cie0Ihl6.js";
2
+ import { n as JSX } from "./vnode-DyFwtMVw.js";
3
3
  import { jsx, jsxDEV, jsxs } from "./jsx-runtime.js";
4
4
  export { Fragment, type JSX, jsx, jsxDEV, jsxs };
@@ -1,5 +1,5 @@
1
- import { i as Fragment, s as View } from "./types-DU46exHq.js";
2
- import { n as JSX, t as h } from "./vnode-BZLcG8lC.js";
1
+ import { o as View, r as Fragment } from "./types-Cie0Ihl6.js";
2
+ import { n as JSX, t as h } from "./vnode-DyFwtMVw.js";
3
3
  //#region src/jsx-runtime.d.ts
4
4
  declare const jsx: (type: string | typeof Fragment | ((p: Record<string, unknown>) => unknown), props: Record<string, unknown> | null, key?: string | number) => ReturnType<typeof h>;
5
5
  declare const jsxs: (type: string | typeof Fragment | ((p: Record<string, unknown>) => unknown), props: Record<string, unknown> | null, key?: string | number) => ReturnType<typeof h>;