ilha 0.15.1 → 0.16.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.
package/dist/index.js CHANGED
@@ -1,51 +1,43 @@
1
- import { _ as isString, f as errorView, i as Fragment, m as isFunction, t as h } from "./vnode-BHUY61Rm.js";
2
- import { C as makeFiber, S as getFiber, _ as structuralEqual, b as clearFiberView, d as interpret, f as atom, g as isAtomHandle, h as instr, m as handleOwner, n as unsafe, p as batch, u as runSetup, v as untrack, x as closeFiber, y as wrapHandle } from "./paint-core-D9VO-oUM.js";
3
- import { n as renderToString, t as mount } from "./mount-DCu8b0hE.js";
1
+ import { _ as IlhaError, c as batch, d as untrack, f as wrapHandle, g as makeFiber, h as getFiber, i as runSetup, l as instr, m as closeFiber, o as interpret, p as clearFiberView, s as atom } from "./start-B-e1dG5U.js";
2
+ import { i as errorView, o as isFunction } from "./shared-vpgYXytd.js";
3
+ import { n as unsafe } from "./paint-core-D1n4lbpF.js";
4
+ import { r as renderToString, t as mount } from "./mount-BwuJsiDH.js";
5
+ import { r as Fragment, t as h } from "./vnode-DJ25Jn3D.js";
4
6
  import * as Atom from "effect/reactivity/Atom";
5
7
  import * as Effect from "effect/Effect";
6
8
  import * as Stream from "effect/Stream";
7
- import * as Exit from "effect/Exit";
8
- import * as Fiber from "effect/Fiber";
9
- import * as Registry from "effect/reactivity/AtomRegistry";
10
- import * as Scope from "effect/Scope";
11
- import * as Queue from "effect/Queue";
12
9
 
13
10
  //#region src/context.ts
14
- const defaults = /* @__PURE__ */ new WeakMap();
15
- const getDefaultAtom = (ctx) => {
16
- const existing = defaults.get(ctx);
17
- if (existing) return existing;
18
- const made = Atom.make(ctx.defaultValue);
19
- defaults.set(ctx, made);
20
- return made;
21
- };
11
+ const fallbacks = /* @__PURE__ */ new WeakMap();
22
12
  const createContext = (defaultValue) => {
23
13
  const id = Symbol("ilha.context");
24
- const Provider = (props) => {
25
- const fiber = getFiber();
26
- const { value: next, children } = props;
27
- const handle = atom(next);
28
- if (!Object.is(untrack(() => handle()), next)) handle.set(next);
29
- fiber.contexts ??= /* @__PURE__ */ new Map();
30
- fiber.contexts.set(id, handle.atom);
31
- return children;
32
- };
33
- return {
34
- Provider,
14
+ const ctx = {
15
+ Provider: ({ value, children }) => {
16
+ const fiber = getFiber();
17
+ const handle = atom.lazy(() => value, { snapshot: false });
18
+ if (!Object.is(untrack(handle), value)) handle.set(value);
19
+ fiber.contexts ??= /* @__PURE__ */ new Map();
20
+ fiber.contexts.set(id, handle.atom);
21
+ return children;
22
+ },
35
23
  defaultValue,
36
24
  id
37
25
  };
26
+ fallbacks.set(ctx, Atom.make(defaultValue));
27
+ return ctx;
38
28
  };
39
29
  /** Read a context value as an atom handle. Subscribes the active fiber on read. */
40
30
  const context = (ctx) => {
41
31
  const fiber = getFiber();
42
- let current = fiber;
43
- while (current) {
44
- const found = current.contexts?.get(ctx.id);
45
- if (found) return wrapHandle(found, fiber);
46
- current = current.parent;
32
+ let found = fallbacks.get(ctx);
33
+ for (let f = fiber; f; f = f.parent) {
34
+ const provided = f.contexts?.get(ctx.id);
35
+ if (provided) {
36
+ found = provided;
37
+ break;
38
+ }
47
39
  }
48
- return wrapHandle(getDefaultAtom(ctx), fiber);
40
+ return wrapHandle(found ?? Atom.make(ctx.defaultValue), fiber);
49
41
  };
50
42
 
51
43
  //#endregion
@@ -53,591 +45,118 @@ const context = (ctx) => {
53
45
  /** Catch subtree failures and paint a fallback. Supports reset(). */
54
46
  const ErrorBoundary = (props) => {
55
47
  const fiber = getFiber();
56
- const remount = () => {
57
- if (fiber.closed) return;
58
- clearFiberView(fiber);
59
- const current = fiber.propsBox?.current ?? props;
60
- runSetup(fiber, () => ErrorBoundary(current));
61
- };
48
+ const current = () => fiber.propsBox?.current ?? props;
62
49
  fiber.handleError = (error) => {
63
50
  if (fiber.closed) return;
64
51
  clearFiberView(fiber);
65
- const { onError, fallback } = fiber.propsBox?.current ?? props;
66
- if (isFunction(onError)) onError(error);
67
- if (isFunction(fallback)) {
68
- fiber.paint(fallback({
69
- error,
70
- reset: remount
71
- }));
72
- return;
73
- }
74
- fiber.paint(errorView(error));
52
+ const { onError, fallback } = current();
53
+ onError?.(error);
54
+ fiber.paint(fallback ? fallback({
55
+ error,
56
+ reset: () => {
57
+ if (!fiber.closed) {
58
+ clearFiberView(fiber);
59
+ runSetup(fiber, () => ErrorBoundary(current()));
60
+ }
61
+ }
62
+ }) : errorView(error));
75
63
  };
76
64
  return props.children;
77
65
  };
78
66
 
79
67
  //#endregion
80
68
  //#region src/watch.ts
81
- const WATCH_ONCE = Symbol("ilha.watch.once");
82
- const newRunCell = () => ({
83
- cleanup: void 0,
84
- ctrl: null,
85
- extra: [],
86
- gen: 0
87
- });
88
- const cancelRun = (cell) => {
89
- cell.gen += 1;
90
- cell.ctrl?.abort();
91
- cell.ctrl = null;
92
- const run = cell.cleanup;
93
- cell.cleanup = void 0;
94
- run?.();
95
- for (const extra of cell.extra.splice(0)) extra();
96
- };
97
- const asCleanup = (value) => isFunction(value) ? value : void 0;
98
- const settleAsync = async (cell, gen, out) => {
99
- try {
100
- const c = await out;
101
- if (cell.gen === gen) cell.cleanup = asCleanup(c);
102
- else asCleanup(c)?.();
103
- } catch (error) {
104
- console.error(error);
105
- }
106
- };
107
- const settleRun = (cell, gen, out) => {
108
- if (out instanceof Promise) {
109
- settleAsync(cell, gen, out);
110
- return;
111
- }
112
- if (cell.gen === gen) cell.cleanup = asCleanup(out);
113
- else asCleanup(out)?.();
114
- };
115
- const runWithSignal = (cell, fn) => {
116
- cancelRun(cell);
117
- const { gen } = cell;
118
- const ctrl = new AbortController();
119
- cell.ctrl = ctrl;
120
- const ctx = {
121
- onCleanup: (extra) => {
122
- if (ctrl.signal.aborted || cell.gen !== gen) {
123
- extra();
124
- return;
125
- }
126
- cell.extra.push(extra);
127
- },
128
- signal: ctrl.signal
129
- };
130
- settleRun(cell, gen, fn(ctx));
131
- };
132
- const runStreamWatch = (fiber, effect) => {
133
- const scope = Scope.forkUnsafe(fiber.scope);
134
- const provided = effect.pipe(Effect.provideService(Registry.AtomRegistry, fiber.registry));
135
- const fork = Effect.runFork(provided);
136
- Effect.runSync(Scope.addFinalizer(scope, Fiber.interrupt(fork)));
137
- return () => {
138
- Effect.runFork(Scope.close(scope, Exit.void));
139
- };
140
- };
141
- const useWatchSlot = (fiber, key, mount, fn) => {
142
- const i = fiber.watchI ?? 0;
143
- fiber.watchI = i + 1;
144
- fiber.watchSlots ??= [];
145
- const existing = fiber.watchSlots[i];
146
- if (existing) {
147
- if (existing.key === key) {
148
- if (existing.fnRef) existing.fnRef.current = fn;
149
- return;
150
- }
151
- existing.dispose();
152
- fiber.watchSlots[i] = void 0;
153
- }
154
- const fnRef = { current: fn };
155
- const unsub = mount((value) => {
156
- fnRef.current(value);
157
- });
158
- const dispose = () => unsub();
159
- fiber.watchSlots[i] = {
160
- dispose,
161
- fnRef,
162
- key
163
- };
164
- };
165
- const sourceCells = /* @__PURE__ */ new WeakMap();
166
- const registerWatch = (fiber, source, fn) => {
167
- const slot = fiber.watchI ?? 0;
168
- let cells = sourceCells.get(fiber);
169
- if (!cells) {
170
- cells = [];
171
- sourceCells.set(fiber, cells);
172
- }
173
- const entry = cells[slot] ?? {
174
- cell: newRunCell(),
175
- fn: void 0
176
- };
177
- cells[slot] = entry;
178
- entry.fn = fn;
179
- const { cell } = entry;
180
- const wrapped = (value) => {
181
- const latest = entry.fn ?? fn;
182
- runWithSignal(cell, (ctx) => latest(value, ctx));
183
- };
184
- const disposeCell = () => {
185
- cancelRun(cell);
186
- };
187
- if (Stream.isStream(source)) {
188
- useWatchSlot(fiber, source, (run) => {
189
- let active = true;
190
- const stop = runStreamWatch(fiber, Stream.runForEach(source, (v) => Effect.sync(() => {
191
- if (active) run(v);
192
- })));
193
- return () => {
194
- active = false;
195
- disposeCell();
196
- stop();
197
- };
198
- }, wrapped);
199
- return;
200
- }
201
- const atom = isAtomHandle(source) ? source.atom : source;
202
- useWatchSlot(fiber, atom, (run) => {
203
- const unsub = fiber.registry.subscribe(atom, run, { immediate: true });
204
- return () => {
205
- disposeCell();
206
- unsub();
207
- };
208
- }, wrapped);
209
- };
210
- /** Run `fn` once on mount. Return a function (or a promise of one) to clean up on unmount. */
211
- const watchOnce = (fn) => {
212
- const fiber = getFiber();
213
- const i = fiber.watchI ?? 0;
214
- fiber.watchI = i + 1;
215
- fiber.watchSlots ??= [];
216
- const existing = fiber.watchSlots[i];
217
- if (existing) {
218
- if (existing.key === WATCH_ONCE) return instr(Effect.void);
219
- existing.dispose();
220
- fiber.watchSlots[i] = void 0;
221
- }
222
- const cell = newRunCell();
223
- runWithSignal(cell, fn);
224
- fiber.watchSlots[i] = {
225
- dispose: () => {
226
- cancelRun(cell);
227
- },
228
- key: WATCH_ONCE
229
- };
230
- return instr(Effect.void);
231
- };
232
- /** Run `fn` when `source` changes — and once on mount. Sync, async, and generator components. */
233
- const watchSource = (source, fn) => {
234
- const fiber = (isAtomHandle(source) ? handleOwner.get(source) : void 0) ?? getFiber();
235
- registerWatch(fiber, source, fn);
236
- return instr(Effect.void);
237
- };
238
- const watch = Object.assign(watchSource, { once: watchOnce });
239
-
240
- //#endregion
241
- //#region src/resource.ts
242
- const sharedCache = /* @__PURE__ */ new Map();
243
- const liveRefetchers = /* @__PURE__ */ new Map();
244
- const cells$1 = /* @__PURE__ */ new WeakMap();
245
- /** Unmount: stop every client cell of one fiber (all keys it ever used). */
246
- const releaseCells = (byKey) => {
247
- for (const [key, entry] of byKey) {
248
- const c = entry;
249
- c.alive = false;
250
- c.ctrl?.abort();
251
- const live = liveRefetchers.get(key);
252
- live?.delete(c.onInvalidate);
253
- if (live && live.size === 0) liveRefetchers.delete(key);
254
- }
255
- byKey.clear();
256
- };
257
- const entryFor = (key) => sharedCache.get(key);
258
- /** Drop the cached value for `key` and refetch it in live components. */
259
- const invalidate = (key) => {
260
- sharedCache.delete(key);
261
- const live = liveRefetchers.get(key);
262
- if (!live) return;
263
- for (const refetch of live) refetch();
264
- };
69
+ const ONCE = Symbol("ilha.watch.once");
265
70
  /**
266
- * Fetch-once, share-everywhere async state for `key`. Concurrent mounts
267
- * dedupe on one in-flight request; remounts seed from the shared cache
268
- * (and revalidate when `staleWhileRevalidate` holds). Call unconditionally
269
- * at the top level of a sync component, before any `await`.
71
+ * Serial runs of one watch slot. Each run gets a fresh signal; starting the
72
+ * next run or cancelling aborts it, which also runs its cleanups.
270
73
  */
271
- const resource = (key, fetcher, opts) => {
272
- const fiber = getFiber();
273
- const data = atom(void 0);
274
- const error = atom(void 0);
275
- const loading = atom(true);
276
- let byKey = cells$1.get(fiber);
277
- if (!byKey) {
278
- byKey = /* @__PURE__ */ new Map();
279
- cells$1.set(fiber, byKey);
280
- }
281
- if (fiber.runtime.ssr) {
282
- let ssrCell = byKey.get(key);
283
- if (!ssrCell) {
284
- const { runtime } = fiber;
285
- const sc = {
286
- ctrl: null,
287
- refetch: async () => {
288
- sc.ctrl?.abort();
289
- const ctrl = new AbortController();
290
- sc.ctrl = ctrl;
291
- sc.sink.loading.set(true);
292
- runtime.begin();
293
- try {
294
- const v = await fetcher(key, { signal: ctrl.signal });
295
- if (!ctrl.signal.aborted) {
296
- sc.sink.data.set(v);
297
- sc.sink.error.set(void 0);
298
- sc.sink.loading.set(false);
299
- }
300
- return v;
301
- } catch (fetchError) {
302
- if (!ctrl.signal.aborted) {
303
- sc.sink.error.set(fetchError);
304
- sc.sink.loading.set(false);
305
- }
306
- throw fetchError;
307
- } finally {
308
- runtime.end();
309
- }
310
- },
311
- sink: {
312
- data,
313
- error,
314
- loading
315
- }
74
+ const makeRunner = () => {
75
+ let ctrl;
76
+ const cancel = () => ctrl?.abort();
77
+ return {
78
+ cancel,
79
+ run: (fn) => {
80
+ cancel();
81
+ ctrl = new AbortController();
82
+ const { signal } = ctrl;
83
+ const onCleanup = (c) => {
84
+ if (!isFunction(c)) return;
85
+ if (signal.aborted) c();
86
+ else signal.addEventListener("abort", () => c(), { once: true });
316
87
  };
317
- Effect.runSync(Scope.addFinalizer(fiber.scope, Effect.sync(() => sc.ctrl?.abort())));
318
- byKey.set(key, sc);
319
- ssrCell = sc;
320
- const settleSsr = async () => {
88
+ const settle = async (pending) => {
321
89
  try {
322
- await sc.refetch();
323
- } catch {}
324
- };
325
- settleSsr();
326
- }
327
- ssrCell.sink = {
328
- data,
329
- error,
330
- loading
331
- };
332
- return {
333
- data,
334
- error,
335
- loading,
336
- refetch: ssrCell.refetch
337
- };
338
- }
339
- let cell = byKey.get(key);
340
- if (!cell) {
341
- const fresh = {
342
- alive: true,
343
- ctrl: null,
344
- onInvalidate: () => {
345
- fresh.revalidate();
346
- },
347
- revalidate: () => {},
348
- sink: {
349
- data,
350
- error,
351
- loading
352
- },
353
- started: false
354
- };
355
- cell = fresh;
356
- byKey.set(key, cell);
357
- let live = liveRefetchers.get(key);
358
- if (!live) {
359
- live = /* @__PURE__ */ new Set();
360
- liveRefetchers.set(key, live);
361
- }
362
- live.add(cell.onInvalidate);
363
- }
364
- cell.sink = {
365
- data,
366
- error,
367
- loading
368
- };
369
- const c = cell;
370
- const fiberCells = byKey;
371
- const adoptShared = async (shared) => {
372
- try {
373
- const v = await shared;
374
- if (c.alive) {
375
- c.sink.data.set(v);
376
- c.sink.error.set(void 0);
377
- c.sink.loading.set(false);
378
- }
379
- } catch (fetchError) {
380
- if (c.alive) {
381
- c.sink.error.set(fetchError);
382
- c.sink.loading.set(false);
383
- }
384
- }
385
- };
386
- const start = () => {
387
- if (!c.alive) return Promise.resolve(c.sink.data());
388
- const hit = entryFor(key);
389
- if (hit?.promise) {
390
- c.sink.loading.set(true);
391
- adoptShared(hit.promise);
392
- return hit.promise;
393
- }
394
- c.ctrl?.abort();
395
- const ctrl = new AbortController();
396
- c.ctrl = ctrl;
397
- c.sink.loading.set(true);
398
- const flight = {};
399
- const run = async () => {
400
- try {
401
- const v = await fetcher(key, { signal: ctrl.signal });
402
- if (entryFor(key)?.promise === flight.promise) sharedCache.set(key, {
403
- data: v,
404
- has: true
405
- });
406
- if (c.alive && !ctrl.signal.aborted) {
407
- c.sink.data.set(v);
408
- c.sink.error.set(void 0);
409
- c.sink.loading.set(false);
410
- }
411
- return v;
412
- } catch (fetchError) {
413
- const cur = entryFor(key);
414
- if (cur?.promise === flight.promise) cur.promise = void 0;
415
- if (c.alive && !ctrl.signal.aborted) {
416
- c.sink.error.set(fetchError);
417
- c.sink.loading.set(false);
90
+ onCleanup(await pending);
91
+ } catch (error) {
92
+ console.error(error);
418
93
  }
419
- throw fetchError;
420
- }
421
- };
422
- const p = run();
423
- flight.promise = p;
424
- const prev = entryFor(key);
425
- sharedCache.set(key, {
426
- data: prev?.data,
427
- has: prev?.has ?? false,
428
- promise: p
429
- });
430
- return p;
431
- };
432
- const settleStart = async () => {
433
- try {
434
- await start();
435
- } catch {}
436
- };
437
- c.revalidate = () => {
438
- settleStart();
439
- };
440
- if (!c.started) {
441
- c.started = true;
442
- const swr = opts?.staleWhileRevalidate ?? true;
443
- const hit = entryFor(key);
444
- if (hit?.promise) {
445
- if (hit.has) {
446
- c.sink.data.set(hit.data);
447
- c.sink.loading.set(false);
448
- }
449
- adoptShared(hit.promise);
450
- } else if (hit?.has) {
451
- c.sink.data.set(hit.data);
452
- c.sink.error.set(void 0);
453
- c.sink.loading.set(false);
454
- if (swr) settleStart();
455
- } else {
456
- const seed = c.sink.data();
457
- if (seed === void 0) settleStart();
458
- else {
459
- sharedCache.set(key, {
460
- data: seed,
461
- has: true
462
- });
463
- if (swr) settleStart();
464
- else c.sink.loading.set(false);
465
- }
466
- }
467
- }
468
- watch.once(() => () => {
469
- releaseCells(fiberCells);
470
- });
471
- return {
472
- data,
473
- error,
474
- loading,
475
- refetch: () => start()
476
- };
477
- };
478
-
479
- //#endregion
480
- //#region src/sse.ts
481
- const cells = /* @__PURE__ */ new WeakMap();
482
- const MAX_BACKOFF_MS = 3e4;
483
- const setStatus = (c, next) => {
484
- c.state = next;
485
- for (const sink of c.sinks.values()) sink.status.set(next);
486
- };
487
- const connect = ({ cell: c, decode, event, retryBase, url }) => {
488
- let attempt = 0;
489
- let timer;
490
- let source;
491
- let detach;
492
- let disposed = false;
493
- const emit = (value) => {
494
- c.last = { value };
495
- for (const sink of c.sinks.values()) sink.latest.set(value);
496
- for (const push of c.targets) push(value);
497
- };
498
- const onMessage = (ev) => {
499
- const raw = ev.data;
500
- if (!isString(raw)) return;
501
- let value;
502
- try {
503
- value = decode(raw);
504
- } catch {
505
- return;
94
+ };
95
+ const out = fn({
96
+ onCleanup,
97
+ signal
98
+ });
99
+ if (out instanceof Promise) settle(out);
100
+ else onCleanup(out);
506
101
  }
507
- emit(value);
508
- };
509
- const open = () => {
510
- if (disposed) return;
511
- setStatus(c, attempt === 0 ? "connecting" : "retrying");
512
- const es = new EventSource(url);
513
- source = es;
514
- const onOpen = () => {
515
- attempt = 0;
516
- setStatus(c, "open");
517
- };
518
- const onError = () => {
519
- if (disposed || es.readyState !== EventSource.CLOSED) return;
520
- detach?.();
521
- es.close();
522
- attempt += 1;
523
- setStatus(c, "retrying");
524
- timer = setTimeout(open, Math.min(retryBase * 2 ** (attempt - 1), MAX_BACKOFF_MS));
525
- };
526
- const messageEvent = event ?? "message";
527
- es.addEventListener(messageEvent, onMessage);
528
- es.addEventListener("open", onOpen);
529
- es.addEventListener("error", onError);
530
- detach = () => {
531
- es.removeEventListener(messageEvent, onMessage);
532
- es.removeEventListener("open", onOpen);
533
- es.removeEventListener("error", onError);
534
- };
535
- };
536
- open();
537
- return () => {
538
- disposed = true;
539
- clearTimeout(timer);
540
- detach?.();
541
- source?.close();
542
- setStatus(c, "closed");
543
102
  };
544
103
  };
545
104
  /**
546
- * Subscribe to server-sent events. Opens one connection per `(url, event)`
547
- * per component, reconnects with backoff, and closes on unmount. SSR renders
548
- * an empty stream with status `"closed"`. Call unconditionally at the top
549
- * level of a component.
105
+ * Claim the fiber's next watch slot. A slot whose key matches keeps its
106
+ * subscription and takes the new callback; otherwise `open` subscribes anew.
550
107
  */
551
- const fromEventSource = (url, opts) => {
108
+ const useWatchSlot = (key, ref, open) => {
552
109
  const fiber = getFiber();
553
- const status = atom("connecting");
554
- const latest = atom(void 0);
555
- const feedKey = `${url}\n${opts?.event ?? ""}`;
556
- let byKey = cells.get(fiber);
557
- if (!byKey) {
558
- byKey = /* @__PURE__ */ new Map();
559
- cells.set(fiber, byKey);
560
- }
561
- let cell = byKey.get(feedKey);
562
- if (!cell) {
563
- const targets = /* @__PURE__ */ new Set();
564
- const stream = fiber.runtime.ssr ? Stream.empty : Stream.callback((queue) => {
565
- const push = (value) => {
566
- Queue.offerUnsafe(queue, value);
567
- };
568
- targets.add(push);
569
- return Effect.acquireRelease(Effect.void, () => Effect.sync(() => targets.delete(push)));
570
- });
571
- cell = {
572
- sinks: /* @__PURE__ */ new Map(),
573
- state: "connecting",
574
- stream,
575
- targets,
576
- users: 0
110
+ const i = fiber.watchI ?? 0;
111
+ fiber.watchI = i + 1;
112
+ fiber.watchSlots ??= [];
113
+ const existing = fiber.watchSlots[i];
114
+ if (existing && existing.key === key) existing.ref.fn = ref.fn;
115
+ else {
116
+ existing?.dispose();
117
+ fiber.watchSlots[i] = {
118
+ dispose: open(fiber),
119
+ key,
120
+ ref
577
121
  };
578
- byKey.set(feedKey, cell);
579
122
  }
580
- const sinkKey = latest.atom;
581
- cell.sinks.set(sinkKey, {
582
- latest,
583
- status
584
- });
585
- const c = cell;
586
- const decode = opts?.schema ?? ((raw) => raw);
587
- const retryBase = opts?.retryBaseMs ?? 1e3;
588
- watch.once(() => {
589
- if (fiber.runtime.ssr || globalThis.EventSource === void 0) {
590
- status.set("closed");
591
- return;
592
- }
593
- c.users += 1;
594
- if (c.stop) {
595
- status.set(c.state);
596
- if (c.last) latest.set(c.last.value);
597
- } else c.stop = connect({
598
- cell: c,
599
- decode,
600
- event: opts?.event,
601
- retryBase,
602
- url
603
- });
123
+ return instr(Effect.void);
124
+ };
125
+ const watch = Object.assign((source, fn) => {
126
+ const ref = { fn };
127
+ const key = isFunction(source) ? source.atom : source;
128
+ return useWatchSlot(key, ref, (fiber) => {
129
+ const runner = makeRunner();
130
+ const emit = (value) => runner.run((ctx) => ref.fn(value, ctx));
131
+ const stop = Stream.isStream(source) ? fiber.run(Stream.runForEach(source, (v) => Effect.sync(() => emit(v)))) : fiber.registry.subscribe(isFunction(source) ? source.atom : source, emit, { immediate: true });
604
132
  return () => {
605
- c.users -= 1;
606
- if (c.users === 0) {
607
- const { stop } = c;
608
- c.stop = void 0;
609
- stop?.();
610
- }
611
- c.sinks.delete(sinkKey);
133
+ runner.cancel();
134
+ stop();
612
135
  };
613
136
  });
614
- return {
615
- latest,
616
- status,
617
- stream: c.stream
618
- };
619
- };
137
+ }, { once: (fn) => useWatchSlot(ONCE, {}, () => {
138
+ const runner = makeRunner();
139
+ runner.run(fn);
140
+ return runner.cancel;
141
+ }) });
620
142
 
621
143
  //#endregion
622
144
  //#region src/when.ts
623
- const when = (stream, body) => instr(Effect.gen(function* whenBody() {
145
+ /** Rerun `body` for every value of `stream`, replacing the previous run. */
146
+ const when = (stream, body) => instr(Effect.suspend(() => {
624
147
  const parent = getFiber();
625
148
  let child;
626
- parent.holes.push({ dispose() {
149
+ const stop = () => {
627
150
  if (child) closeFiber(child);
628
- child = void 0;
629
- } });
151
+ };
152
+ parent.holes.push({ dispose: stop });
630
153
  const base = stream;
631
- const src = parent.runtime.ssr ? base.pipe(Stream.take(1)) : base;
632
- yield* Stream.runForEach(src, (value) => Effect.sync(() => {
633
- if (child) closeFiber(child);
634
- child = makeFiber(parent.runtime, parent.root, parent.paintFn, {
635
- onFail: parent.fail,
636
- parent
637
- });
638
- interpret((() => body(value)), child);
639
- }));
154
+ return Stream.runForEach(parent.runtime.ssr ? Stream.take(base, 1) : base, (value) => Effect.sync(() => {
155
+ stop();
156
+ child = makeFiber(parent.runtime, parent.root, parent.paintFn, { parent });
157
+ interpret(() => body(value), child);
158
+ })).pipe(Effect.asVoid);
640
159
  }));
641
160
 
642
161
  //#endregion
643
- export { ErrorBoundary, Fragment, atom, batch, context, createContext, fromEventSource, h, invalidate, mount, renderToString, resource, structuralEqual, unsafe, untrack, watch, when };
162
+ export { ErrorBoundary, Fragment, IlhaError, atom, batch, context, createContext, h, mount, renderToString, unsafe, untrack, watch, when };