ilha 0.14.0 → 0.14.1

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
 
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-CzFZBgZG.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-CzFZBgZG.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>;
@@ -40,6 +40,12 @@ const runInScope = (effect, registry, scope, onOk, onErr, onDone) => {
40
40
  function closeFiber(fiber) {
41
41
  if (fiber.closed) return;
42
42
  fiber.closed = true;
43
+ fiber.trackRestore?.();
44
+ fiber.trackRestore = void 0;
45
+ fiber.renderSub?.();
46
+ fiber.renderSub = void 0;
47
+ for (const slot of fiber.watchSlots ?? []) slot?.dispose();
48
+ fiber.watchSlots = [];
43
49
  for (const h of fiber.holes) h.dispose();
44
50
  fiber.holes = [];
45
51
  Effect.runFork(Scope.close(fiber.scope, Exit.void));
@@ -150,11 +156,108 @@ function isAtomHandle(x) {
150
156
  return typeof x === "function" && x.$$atom === 1;
151
157
  }
152
158
  let trackGet;
159
+ const trackStack = [];
160
+ function beginPrimitiveFrame(fiber) {
161
+ const used = fiber.primitiveI ?? 0;
162
+ const slots = fiber.primitives?.length ?? 0;
163
+ if (used > 0 && used !== slots) console.warn(`ilha: atom/watch slot count changed between renders (${slots} → ${used}); keep primitive order stable`);
164
+ fiber.primitiveI = 0;
165
+ fiber.watchI = 0;
166
+ }
167
+ function readTracked(atom, registry) {
168
+ const frame = trackStack.at(-1);
169
+ const active = getActiveFiber();
170
+ if (trackGet && frame && active === frame.fiber) return trackGet(atom);
171
+ return registry.get(atom);
172
+ }
173
+ function usePrimitiveSlot(fiber, make) {
174
+ const i = fiber.primitiveI ?? 0;
175
+ fiber.primitiveI = i + 1;
176
+ fiber.primitives ??= [];
177
+ const existing = fiber.primitives[i];
178
+ if (existing) return {
179
+ atom: existing,
180
+ fresh: false
181
+ };
182
+ const a = make();
183
+ fiber.primitives[i] = a;
184
+ return {
185
+ atom: a,
186
+ fresh: true
187
+ };
188
+ }
189
+ function withTrackGetRun(fiber, fn, onOk, onErr) {
190
+ const deps = /* @__PURE__ */ new Set();
191
+ const get = (a) => {
192
+ deps.add(a);
193
+ return fiber.registry.get(a);
194
+ };
195
+ const restore = () => {
196
+ if (fiber.trackRestore !== restore) return;
197
+ const top = trackStack.pop();
198
+ if (top?.get !== get) {
199
+ if (top) trackStack.push(top);
200
+ const i = trackStack.findIndex((frame) => frame.get === get);
201
+ if (i >= 0) trackStack.splice(i, 1);
202
+ }
203
+ trackGet = trackStack.at(-1)?.get;
204
+ fiber.trackRestore = void 0;
205
+ };
206
+ trackStack.push({
207
+ fiber,
208
+ deps,
209
+ get
210
+ });
211
+ trackGet = get;
212
+ fiber.trackRestore = restore;
213
+ try {
214
+ const result = withFiber(fiber, () => fn());
215
+ if (result instanceof Promise) {
216
+ restore();
217
+ result.then((value) => {
218
+ if (fiber.closed) return;
219
+ onOk({
220
+ value,
221
+ deps
222
+ });
223
+ }).catch((e) => {
224
+ if (fiber.closed) return;
225
+ onErr(e);
226
+ });
227
+ return;
228
+ }
229
+ restore();
230
+ onOk({
231
+ value: result,
232
+ deps
233
+ });
234
+ } catch (e) {
235
+ restore();
236
+ onErr(e);
237
+ }
238
+ }
239
+ function subscribeRenderDeps(fiber, deps, run) {
240
+ if (deps.size === 0) return () => {};
241
+ let scheduled = false;
242
+ const schedule = () => {
243
+ if (scheduled) return;
244
+ scheduled = true;
245
+ fiber.runtime.later(() => {
246
+ scheduled = false;
247
+ run();
248
+ });
249
+ };
250
+ const unsubs = [];
251
+ for (const dep of deps) unsubs.push(fiber.registry.subscribe(dep, schedule));
252
+ return () => {
253
+ for (const unsub of unsubs) unsub();
254
+ };
255
+ }
153
256
  function wrapHandle(atom, fiber) {
154
257
  const registry = fiber.registry;
155
258
  const read = (() => {
156
259
  guardHandleFiber(read);
157
- return trackGet ? trackGet(atom) : registry.get(atom);
260
+ return readTracked(atom, registry);
158
261
  });
159
262
  handleOwner.set(read, fiber);
160
263
  Object.defineProperties(read, {
@@ -171,34 +274,40 @@ function wrapHandle(atom, fiber) {
171
274
  });
172
275
  return read;
173
276
  }
174
- function atom(init) {
277
+ function makeAtom(init) {
175
278
  const fiber = getFiber();
176
- const computed = typeof init === "function" && !Atom.isAtom(init) && !Effect.isEffect(init) && !Stream.isStream(init);
177
- let seed = init;
178
279
  const snap = fiber.runtime.hydrateValues;
179
- if (snap && !Atom.isAtom(init) && !Effect.isEffect(init) && !Stream.isStream(init)) {
180
- const v = snap[fiber.runtime.hydrateI++];
181
- if (!computed && v !== void 0) seed = v;
182
- }
183
- let a;
184
- if (Atom.isAtom(seed)) a = seed;
185
- else if (Effect.isEffect(seed) || Stream.isStream(seed)) a = Atom.make(seed);
186
- else if (computed) {
187
- const fn = init;
188
- a = Atom.make((get) => {
189
- const prev = trackGet;
190
- trackGet = (other) => get(other);
191
- try {
192
- return fn();
193
- } finally {
194
- trackGet = prev;
195
- }
196
- });
197
- } else a = Atom.make(seed);
280
+ const { atom: a, fresh } = usePrimitiveSlot(fiber, () => {
281
+ let seed = init;
282
+ if (snap && !Atom.isAtom(init) && !Effect.isEffect(init) && !Stream.isStream(init)) {
283
+ const v = snap[fiber.runtime.hydrateI++];
284
+ if (v !== void 0) seed = v;
285
+ }
286
+ if (Atom.isAtom(seed)) return seed;
287
+ if (Effect.isEffect(seed) || Stream.isStream(seed)) return Atom.make(seed);
288
+ return Atom.make(seed);
289
+ });
290
+ const handle = wrapHandle(a, fiber);
291
+ if (fresh && fiber.runtime.ssr) fiber.runtime.ssrValues.push(fiber.registry.get(a));
292
+ return handle;
293
+ }
294
+ function atomLazy(init) {
295
+ const fiber = getFiber();
296
+ const snap = fiber.runtime.hydrateValues;
297
+ const { atom: a, fresh } = usePrimitiveSlot(fiber, () => {
298
+ let seed = init();
299
+ if (snap) {
300
+ const v = snap[fiber.runtime.hydrateI++];
301
+ if (v !== void 0) seed = v;
302
+ }
303
+ return Atom.make(seed);
304
+ });
198
305
  const handle = wrapHandle(a, fiber);
199
- if (fiber.runtime.ssr) fiber.runtime.ssrValues.push(fiber.registry.get(a));
306
+ if (fresh && fiber.runtime.ssr) fiber.runtime.ssrValues.push(fiber.registry.get(a));
200
307
  return handle;
201
308
  }
309
+ const atom = Object.assign(makeAtom, { lazy: atomLazy });
310
+ const batch = Atom.batch;
202
311
 
203
312
  //#endregion
204
313
  //#region src/errors.ts
@@ -396,17 +505,17 @@ function morphChildren(fromParent, toParent) {
396
505
  if (fromNode instanceof Element) {
397
506
  const fromKey = morphKeyOf(fromNode);
398
507
  if (fromKey !== null && fromKey !== toKey && toKeys.has(fromKey)) {
399
- fromParent.insertBefore(toNode.cloneNode(true), fromNode);
508
+ fromParent.insertBefore(toNode, fromNode);
400
509
  continue;
401
510
  }
402
511
  }
403
512
  }
404
513
  if (!fromNode) {
405
- fromParent.appendChild(toNode.cloneNode(true));
514
+ fromParent.appendChild(toNode);
406
515
  continue;
407
516
  }
408
517
  if (fromNode.nodeType !== toNode.nodeType) {
409
- fromParent.replaceChild(toNode.cloneNode(true), fromNode);
518
+ fromParent.replaceChild(toNode, fromNode);
410
519
  continue;
411
520
  }
412
521
  if (fromNode.nodeType === 3 || fromNode.nodeType === 8) {
@@ -417,7 +526,7 @@ function morphChildren(fromParent, toParent) {
417
526
  const fromEl = fromNode;
418
527
  const toEl = toNode;
419
528
  if (fromEl.localName !== toEl.localName) {
420
- fromParent.replaceChild(toEl.cloneNode(true), fromEl);
529
+ fromParent.replaceChild(toEl, fromEl);
421
530
  continue;
422
531
  }
423
532
  const slotId = toEl.getAttribute(SLOT_ATTR);
@@ -552,19 +661,39 @@ function finish(fiber, view) {
552
661
  fiber.park();
553
662
  }
554
663
  function runSetup(fiber, fn) {
555
- try {
556
- const out = withFiber(fiber, () => fn());
557
- if (isGen(out)) interpret((() => out), fiber);
558
- else {
559
- fiber.runtime.begin();
560
- Promise.resolve(out).then((view) => {
561
- withFiber(fiber, () => finish(fiber, view));
562
- fiber.runtime.end();
563
- }, (e) => {
664
+ const disposeRenderSub = () => {
665
+ fiber.renderSub?.();
666
+ fiber.renderSub = void 0;
667
+ };
668
+ const commit = (out, deps) => {
669
+ if (isGen(out)) {
670
+ disposeRenderSub();
671
+ interpret((() => out), fiber);
672
+ return;
673
+ }
674
+ disposeRenderSub();
675
+ if (!fiber.runtime.ssr) fiber.renderSub = subscribeRenderDeps(fiber, deps, render);
676
+ withFiber(fiber, () => finish(fiber, out));
677
+ };
678
+ const render = () => {
679
+ if (fiber.closed) return;
680
+ fiber.runtime.begin();
681
+ beginPrimitiveFrame(fiber);
682
+ withTrackGetRun(fiber, () => fn(), ({ value: out, deps }) => {
683
+ try {
684
+ commit(out, deps);
685
+ } catch (e) {
564
686
  withFiber(fiber, () => fiber.fail(asFailure(e)));
687
+ } finally {
565
688
  fiber.runtime.end();
566
- });
567
- }
689
+ }
690
+ }, (e) => {
691
+ withFiber(fiber, () => fiber.fail(asFailure(e)));
692
+ fiber.runtime.end();
693
+ });
694
+ };
695
+ try {
696
+ render();
568
697
  } catch (e) {
569
698
  fiber.fail(asFailure(e));
570
699
  }
@@ -578,9 +707,16 @@ const ISLAND_SLOT_TAG = Symbol.for("ilha.islandSlotTag");
578
707
  function insert(fiber, nodes) {
579
708
  for (const n of nodes) fiber.root.appendChild(n);
580
709
  }
581
- function disposeHoles(fiber) {
582
- for (const h of fiber.holes) h.dispose();
583
- fiber.holes = [];
710
+ function disposeHoles(fiber, opts) {
711
+ const keep = [];
712
+ for (const h of fiber.holes) {
713
+ if (opts?.morph && h.keepOnMorph) {
714
+ keep.push(h);
715
+ continue;
716
+ }
717
+ h.dispose();
718
+ }
719
+ fiber.holes = keep;
584
720
  }
585
721
  function skip(x) {
586
722
  return x === null || x === void 0 || typeof x === "boolean";
@@ -711,17 +847,56 @@ function errorView(e) {
711
847
  children: [failureMessage(e)]
712
848
  };
713
849
  }
714
- function trackHole(parent, hole, extra) {
715
- parent.holes.push({ dispose() {
716
- extra?.();
717
- closeFiber(hole);
718
- } });
850
+ function findReusableAtomHost(fiber, atom) {
851
+ for (const h of fiber.holes) if (h.atom === atom && h.host?.isConnected && h.holeFiber && !h.holeFiber.closed) return {
852
+ host: h.host,
853
+ hole: h.holeFiber
854
+ };
855
+ }
856
+ function refreshAtomHost(fiber, atom, host, hole) {
857
+ if (hole.closed || !host.isConnected) return;
858
+ const next = unwrap(fiber.registry.get(atom), void 0);
859
+ if (next === KEEP) return;
860
+ paintHole(hole, next);
861
+ }
862
+ function refreshAllAtomHosts(fiber) {
863
+ for (const h of fiber.holes) if (h.atom && h.host && h.holeFiber) refreshAtomHost(fiber, h.atom, h.host, h.holeFiber);
864
+ }
865
+ function pruneDisconnectedAtomHoles(fiber) {
866
+ fiber.holes = fiber.holes.filter((h) => {
867
+ if (!h.atom || !h.host || !h.holeFiber) return true;
868
+ if (h.host.isConnected) return true;
869
+ h.dispose();
870
+ return false;
871
+ });
872
+ }
873
+ function atomHostPlaceholder(host) {
874
+ const placeholder = document.createElement("span");
875
+ const slot = host.getAttribute(SLOT_ATTR);
876
+ if (slot !== null) placeholder.setAttribute(SLOT_ATTR, slot);
877
+ placeholder.style.display = "contents";
878
+ return placeholder;
879
+ }
880
+ function trackHole(parent, hole, extra, meta) {
881
+ parent.holes.push({
882
+ keepOnMorph: !!(meta?.atom && meta?.host) || !!meta?.keyed,
883
+ atom: meta?.atom,
884
+ host: meta?.host,
885
+ holeFiber: hole,
886
+ dispose() {
887
+ extra?.();
888
+ closeFiber(hole);
889
+ }
890
+ });
719
891
  }
720
892
  function materialize(view, fiber) {
721
893
  if (skip(view)) return [];
722
894
  if (typeof view === "string" || typeof view === "number" || typeof view === "bigint") return [document.createTextNode(String(view))];
723
895
  if (isAtomHandle(view)) {
896
+ const reused = findReusableAtomHost(fiber, view.atom);
897
+ if (reused) return [atomHostPlaceholder(reused.host)];
724
898
  const { fiber: hole, nodes } = openHole(fiber);
899
+ const host = nodes[0];
725
900
  let unsub = () => {};
726
901
  fiber.runtime.later(() => {
727
902
  if (hole.closed) return;
@@ -734,7 +909,10 @@ function materialize(view, fiber) {
734
909
  paintHole(hole, next);
735
910
  }, { immediate: true });
736
911
  });
737
- trackHole(fiber, hole, () => unsub());
912
+ trackHole(fiber, hole, () => unsub(), {
913
+ atom: view.atom,
914
+ host
915
+ });
738
916
  return nodes;
739
917
  }
740
918
  if (Stream.isStream(view)) {
@@ -792,7 +970,7 @@ function materialize(view, fiber) {
792
970
  fiber.runtime.later(() => {
793
971
  if (!hole.closed) runSetup(hole, () => type(hole.propsBox.current));
794
972
  });
795
- trackHole(fiber, hole);
973
+ trackHole(fiber, hole, void 0, k ? { keyed: true } : void 0);
796
974
  return nodes;
797
975
  }
798
976
  const el = document.createElement(view.type);
@@ -838,10 +1016,13 @@ function paint(fiber, view) {
838
1016
  }
839
1017
  }
840
1018
  if (fiber.liveEl && isVNode(view) && typeof view.type === "string" && fiber.liveEl.localName === view.type) {
1019
+ disposeHoles(fiber, { morph: true });
841
1020
  applyProps(fiber.liveEl, view.props, fiber);
842
1021
  const tmp = document.createElement(fiber.liveEl.localName);
843
1022
  for (const c of view.children) for (const n of materialize(c, fiber)) tmp.appendChild(n);
844
1023
  morphInner(fiber.liveEl, tmp);
1024
+ refreshAllAtomHosts(fiber);
1025
+ pruneDisconnectedAtomHoles(fiber);
845
1026
  return;
846
1027
  }
847
1028
  disposeHoles(fiber);
@@ -989,4 +1170,4 @@ async function renderToString(fn, opts) {
989
1170
  }
990
1171
 
991
1172
  //#endregion
992
- export { atom as a, isAtomHandle as c, makeFiber as d, interpret as i, closeFiber as l, renderToString as n, handleOwner as o, paint as r, instr as s, mount as t, getFiber as u };
1173
+ export { atom as a, instr as c, getFiber as d, makeFiber as f, interpret as i, isAtomHandle as l, renderToString as n, batch as o, paint as r, handleOwner as s, mount as t, closeFiber as u };
@@ -28,6 +28,5 @@ interface AtomHandle<A> {
28
28
  set(next: A): void;
29
29
  update(f: (current: A) => A): void;
30
30
  }
31
- type Done<A> = (value: A) => void;
32
31
  //#endregion
33
- export { Instruction as a, Yielded as c, Fragment as i, Component as n, VNode as o, Done as r, View as s, AtomHandle as t };
32
+ export { VNode as a, Instruction as i, Component as n, View as o, Fragment as r, Yielded as s, AtomHandle as t };
@@ -1,4 +1,4 @@
1
- import { i as Fragment, o as VNode, s as View } from "./types-DU46exHq.js";
1
+ import { a as VNode, o as View, r as Fragment } from "./types-Cie0Ihl6.js";
2
2
  //#region src/jsx-types.d.ts
3
3
  declare namespace JSX {
4
4
  type Element = View;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Generator-first island UI on Effect",
5
5
  "keywords": [
6
6
  "effect",