ilha 0.15.2 → 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/AGENTS.md +102 -0
- package/LICENSE +21 -0
- package/MIGRATIONS.json +96 -0
- package/MIGRATIONS.md +198 -0
- package/README.md +9 -26
- package/dist/debug.d.ts +45 -0
- package/dist/debug.js +217 -0
- package/dist/index.d.ts +30 -50
- package/dist/index.js +105 -586
- package/dist/jsx-dev-runtime.d.ts +2 -2
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +1 -1
- package/dist/mount-3Ggg67T0.d.ts +14 -0
- package/dist/mount-BwuJsiDH.js +585 -0
- package/dist/paint-core-D1n4lbpF.js +350 -0
- package/dist/renderer.d.ts +30 -66
- package/dist/renderer.js +18 -50
- package/dist/{runtime-Baq7MtkE.d.ts → runtime-BCdKBqYp.d.ts} +29 -35
- package/dist/shared-vpgYXytd.js +36 -0
- package/dist/snapshot-BDC74QwI.d.ts +9 -0
- package/dist/start-B-e1dG5U.js +515 -0
- package/dist/test.d.ts +66 -0
- package/dist/test.js +179 -0
- package/dist/{types-DsdtZMhG.d.ts → types-NhEiBvwy.d.ts} +39 -46
- package/dist/vnode-DJ25Jn3D.js +22 -0
- package/dist/{vnode-CpSQZnLM.d.ts → vnode-DskooLPD.d.ts} +10 -2
- package/llms.txt +37 -0
- package/oxlint.cjs +596 -0
- package/package.json +20 -14
- package/dist/define.d.ts +0 -4
- package/dist/define.js +0 -18
- package/dist/mount-DKY_jSsG.js +0 -513
- package/dist/paint-core-B7jENZwS.js +0 -1693
- package/dist/vnode-BHUY61Rm.js +0 -100
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as View, b as WatchOnceCleanup, c as IlhaRuntime, l as Instruction, n as AtomHandle, p as PropBag, v as WatchCallback, y as WatchContext } from "./types-NhEiBvwy.js";
|
|
2
|
+
import "./snapshot-BDC74QwI.js";
|
|
2
3
|
import * as Atom from "effect/reactivity/Atom";
|
|
3
4
|
import * as Effect from "effect/Effect";
|
|
4
5
|
import * as Stream from "effect/Stream";
|
|
@@ -10,48 +11,42 @@ type WatchValue = string | number | boolean | bigint | null | undefined | readon
|
|
|
10
11
|
interface WatchRecord {
|
|
11
12
|
readonly [key: string]: WatchValue | undefined;
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
type WatchRun = (ctx: WatchContext) => WatchOnceCleanup | undefined | Promise<WatchOnceCleanup | undefined>;
|
|
15
|
+
interface WatchRef {
|
|
16
|
+
/** Latest callback; a rerender swaps it without resubscribing. */
|
|
17
|
+
fn?: WatchCallback<never>;
|
|
18
|
+
}
|
|
16
19
|
interface WatchSlot {
|
|
20
|
+
/** Source identity: the watched atom or stream, or the `watch.once` marker. */
|
|
17
21
|
key: WatchKey;
|
|
18
|
-
|
|
19
|
-
current: (value: WatchValue) => void;
|
|
20
|
-
};
|
|
22
|
+
ref: WatchRef;
|
|
21
23
|
dispose: () => void;
|
|
22
24
|
}
|
|
25
|
+
declare const ONCE: unique symbol;
|
|
26
|
+
type WatchKey = Atom.Atom<WatchValue> | Stream.Stream<WatchValue, unknown, unknown> | typeof ONCE;
|
|
23
27
|
interface WatchFn {
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
/** Run `fn` with the current value of `source` now and on every change. */
|
|
29
|
+
<A extends WatchValue>(source: AtomHandle<A> | Atom.Atom<A> | Stream.Stream<A, unknown, unknown>, fn: WatchCallback<A>): Instruction<void>;
|
|
30
|
+
/** Run `fn` once on mount; its returned cleanup runs on unmount. */
|
|
31
|
+
once: (fn: WatchRun) => Instruction<void>;
|
|
26
32
|
}
|
|
27
33
|
declare const watch: WatchFn;
|
|
28
34
|
//#endregion
|
|
29
35
|
//#region src/runtime.d.ts
|
|
36
|
+
/** A disposable a paint leaves behind: a child fiber, subscription, or ref. */
|
|
30
37
|
interface Hole {
|
|
31
38
|
dispose: () => void;
|
|
32
|
-
|
|
39
|
+
/** Survives repaints that reach it again (components, atom holes). */
|
|
40
|
+
keep?: boolean;
|
|
33
41
|
atom?: Atom.Atom<unknown>;
|
|
34
|
-
|
|
35
|
-
holeFiber?: FiberLocal;
|
|
36
|
-
islandSlot?: IslandSlot;
|
|
37
|
-
}
|
|
38
|
-
interface IslandSlot {
|
|
39
|
-
type: string | Fragment | ComponentFn;
|
|
40
|
-
host: Element;
|
|
41
|
-
updateProps?: (props?: PropBag) => void;
|
|
42
|
-
unmount?: () => void;
|
|
43
|
-
}
|
|
44
|
-
interface IslandFrame {
|
|
45
|
-
i: number;
|
|
46
|
-
slots: (IslandSlot | undefined)[];
|
|
47
|
-
}
|
|
48
|
-
interface ComponentSlot {
|
|
49
|
-
type: unknown;
|
|
50
|
-
hole: FiberLocal;
|
|
42
|
+
fiber?: FiberLocal;
|
|
51
43
|
}
|
|
52
44
|
interface ComponentFrame {
|
|
53
45
|
i: number;
|
|
54
|
-
slots:
|
|
46
|
+
slots: {
|
|
47
|
+
type: unknown;
|
|
48
|
+
hole: FiberLocal;
|
|
49
|
+
}[];
|
|
55
50
|
}
|
|
56
51
|
interface FiberLocal {
|
|
57
52
|
root: ParentNode;
|
|
@@ -63,18 +58,16 @@ interface FiberLocal {
|
|
|
63
58
|
closed: boolean;
|
|
64
59
|
hydrate?: boolean;
|
|
65
60
|
keyedHoles?: Map<string, FiberLocal>;
|
|
66
|
-
/** Counts this fiber's paints
|
|
67
|
-
|
|
68
|
-
/** On a
|
|
69
|
-
|
|
70
|
-
liveEl?: Element;
|
|
61
|
+
/** Counts this fiber's paints. */
|
|
62
|
+
gen?: number;
|
|
63
|
+
/** On a hole: the parent's `gen` of the last paint that reached it. */
|
|
64
|
+
seen?: number;
|
|
71
65
|
primitiveI?: number;
|
|
72
66
|
primitives?: Atom.Atom<unknown>[];
|
|
73
67
|
/** Releases for the state atoms this fiber keeps in the registry. */
|
|
74
68
|
primitiveHolds?: (() => void)[];
|
|
75
69
|
watchI?: number;
|
|
76
70
|
watchSlots?: WatchSlot[];
|
|
77
|
-
islandFrame?: IslandFrame;
|
|
78
71
|
componentFrame?: ComponentFrame;
|
|
79
72
|
componentType?: unknown;
|
|
80
73
|
renderSub?: () => void;
|
|
@@ -89,7 +82,8 @@ interface FiberLocal {
|
|
|
89
82
|
handleError?: (error: Error) => void;
|
|
90
83
|
paintFn: (fiber: FiberLocal, view: View) => void;
|
|
91
84
|
paint: (view: View) => void;
|
|
92
|
-
|
|
85
|
+
/** Fork `effect` in this fiber's scope; returns an interruptor. */
|
|
86
|
+
run: <A, E>(effect: Effect.Effect<A, E, AtomRegistry>, onOk?: (a: A) => void, onErr?: (e: E) => void) => () => void;
|
|
93
87
|
fail: <E>(e: E) => void;
|
|
94
88
|
}
|
|
95
89
|
declare const getFiber: () => FiberLocal;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
2
|
+
|
|
3
|
+
//#region src/shared.ts
|
|
4
|
+
const KEY_ATTR = "data-ilha-key";
|
|
5
|
+
const SLOT_ATTR = "data-ilha-slot";
|
|
6
|
+
const isString = (value) => typeof value === "string";
|
|
7
|
+
const isFunction = (value) => typeof value === "function";
|
|
8
|
+
/** Non-null, non-array object. */
|
|
9
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
/** Whether `value` carries ilha's runtime brand `key` (set to `1`). */
|
|
11
|
+
const hasBrand = (value, key) => (isObject(value) || isFunction(value)) && Object.getOwnPropertyDescriptor(value, key)?.value === 1;
|
|
12
|
+
/** Arrays and other iterable objects (not strings). */
|
|
13
|
+
const isIterable = (value) => (Array.isArray(value) || isObject(value)) && Symbol.iterator in value;
|
|
14
|
+
const noop = () => {};
|
|
15
|
+
/** Views that paint nothing. */
|
|
16
|
+
const skip = (x) => x === null || x === void 0 || x === true || x === false;
|
|
17
|
+
const KEEP = Symbol("ilha.keep");
|
|
18
|
+
/** Normalize any thrown value into an `Error`. */
|
|
19
|
+
const toError = (e) => e instanceof Error ? e : new Error(String(e));
|
|
20
|
+
/** Unwrap an atom value for painting; `AsyncResult` waits keep the last view. */
|
|
21
|
+
const unwrap = (value, keep) => {
|
|
22
|
+
if (!AsyncResult.isAsyncResult(value)) return value;
|
|
23
|
+
if (AsyncResult.isWaiting(value) && keep !== void 0) return KEEP;
|
|
24
|
+
if (AsyncResult.isFailure(value)) return String(AsyncResult.error(value) ?? "error");
|
|
25
|
+
const v = AsyncResult.value(value);
|
|
26
|
+
return v._tag === "Some" ? v.value : "";
|
|
27
|
+
};
|
|
28
|
+
const errorView = (e) => ({
|
|
29
|
+
$$ilha: 1,
|
|
30
|
+
children: [toError(e).message],
|
|
31
|
+
props: { "data-ilha-error": "" },
|
|
32
|
+
type: "pre"
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { hasBrand as a, isObject as c, skip as d, toError as f, errorView as i, isString as l, KEY_ATTR as n, isFunction as o, unwrap as p, SLOT_ATTR as r, isIterable as s, KEEP as t, noop as u };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { u as JsonText } from "./types-NhEiBvwy.js";
|
|
2
|
+
//#region src/snapshot.d.ts
|
|
3
|
+
/** JSON values accepted in SSR hydrate snapshots. */
|
|
4
|
+
type SnapshotValue = JsonText | SnapshotObject | readonly SnapshotValue[];
|
|
5
|
+
interface SnapshotObject {
|
|
6
|
+
readonly [key: string]: SnapshotValue | undefined;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { SnapshotValue as t };
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { a as hasBrand, f as toError, i as errorView, o as isFunction, u as noop } from "./shared-vpgYXytd.js";
|
|
2
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Equal from "effect/Equal";
|
|
5
|
+
import * as Stream from "effect/Stream";
|
|
6
|
+
import * as Cause from "effect/Cause";
|
|
7
|
+
import * as Context from "effect/Context";
|
|
8
|
+
import * as Exit from "effect/Exit";
|
|
9
|
+
import * as Registry from "effect/reactivity/AtomRegistry";
|
|
10
|
+
import * as Scope from "effect/Scope";
|
|
11
|
+
|
|
12
|
+
//#region src/diagnostics.ts
|
|
13
|
+
const DOCS_ORIGIN = "https://ilha.build/errors";
|
|
14
|
+
const CATALOG = {
|
|
15
|
+
ILHA001: {
|
|
16
|
+
fix: "Call atom(), watch(), when() and context() at the top level of a component rendered through mount() or renderToString().",
|
|
17
|
+
message: "A primitive was called with no active component"
|
|
18
|
+
},
|
|
19
|
+
ILHA002: {
|
|
20
|
+
fix: "Call atom() and watch() unconditionally and in the same order on every render; move conditions inside the primitive.",
|
|
21
|
+
message: "The number of atom/watch slots changed between renders"
|
|
22
|
+
},
|
|
23
|
+
ILHA003: {
|
|
24
|
+
fix: "Pass the value or a prop to the other island, or share state with createContext() and context().",
|
|
25
|
+
message: "An atom handle was used from a different island"
|
|
26
|
+
},
|
|
27
|
+
ILHA004: {
|
|
28
|
+
fix: "Yield one instruction at a time and let it finish before the generator resumes.",
|
|
29
|
+
message: "A generator component resumed while an instruction was in flight"
|
|
30
|
+
},
|
|
31
|
+
ILHA005: {
|
|
32
|
+
fix: "Give every keyed component a unique key within its parent component.",
|
|
33
|
+
message: "Two sibling components use the same key"
|
|
34
|
+
},
|
|
35
|
+
ILHA006: {
|
|
36
|
+
fix: "Render the same component on the server and the client, and keep primitive order stable.",
|
|
37
|
+
message: "Hydration mismatch, falling back to a full mount"
|
|
38
|
+
},
|
|
39
|
+
ILHA007: {
|
|
40
|
+
fix: "Do not interpolate untrusted text into <script> or <style>; serialize data as JSON with the closing tag escaped.",
|
|
41
|
+
message: "Unsafe raw text in a <script> or <style> element"
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/** Error thrown by ilha with a stable `code`, a `fix` hint and a `docs` URL. */
|
|
45
|
+
var IlhaError = class extends Error {
|
|
46
|
+
code;
|
|
47
|
+
fix;
|
|
48
|
+
docs;
|
|
49
|
+
constructor(code, detail) {
|
|
50
|
+
const { message, fix } = CATALOG[code];
|
|
51
|
+
const docs = `${DOCS_ORIGIN}/${code}`;
|
|
52
|
+
const head = detail ? `${message} (${detail})` : message;
|
|
53
|
+
super(`[${code}] ${head}. Fix: ${fix} See ${docs}`);
|
|
54
|
+
this.name = "IlhaError";
|
|
55
|
+
this.code = code;
|
|
56
|
+
this.fix = fix;
|
|
57
|
+
this.docs = docs;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/** Build an {@link IlhaError} for `code`. */
|
|
61
|
+
const ilhaError = (code, detail) => new IlhaError(code, detail);
|
|
62
|
+
const warned = /* @__PURE__ */ new Set();
|
|
63
|
+
/** Warn once per `code` + `detail` through `console.warn`. */
|
|
64
|
+
const ilhaWarn = (code, detail) => {
|
|
65
|
+
const id = `${code}:${detail ?? ""}`;
|
|
66
|
+
if (warned.has(id)) return;
|
|
67
|
+
warned.add(id);
|
|
68
|
+
console.warn(ilhaError(code, detail).message);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/runtime.ts
|
|
73
|
+
const stack = [];
|
|
74
|
+
const getActiveFiber = () => stack.findLast((x) => !x.closed);
|
|
75
|
+
const getFiber = () => {
|
|
76
|
+
const f = getActiveFiber();
|
|
77
|
+
if (!f) throw ilhaError("ILHA001");
|
|
78
|
+
return f;
|
|
79
|
+
};
|
|
80
|
+
const withFiber = (fiber, fn) => {
|
|
81
|
+
stack.push(fiber);
|
|
82
|
+
try {
|
|
83
|
+
return fn();
|
|
84
|
+
} finally {
|
|
85
|
+
stack.pop();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/** Clear render subscriptions and child holes without closing the fiber scope. */
|
|
89
|
+
const clearFiberView = (fiber) => {
|
|
90
|
+
fiber.renderSub?.();
|
|
91
|
+
fiber.renderSub = void 0;
|
|
92
|
+
for (const slot of fiber.watchSlots ?? []) slot?.dispose();
|
|
93
|
+
for (const release of fiber.primitiveHolds ?? []) release();
|
|
94
|
+
for (const h of fiber.holes) h.dispose();
|
|
95
|
+
fiber.watchSlots = [];
|
|
96
|
+
fiber.watchI = 0;
|
|
97
|
+
fiber.primitives = void 0;
|
|
98
|
+
fiber.primitiveI = 0;
|
|
99
|
+
fiber.primitiveHolds = void 0;
|
|
100
|
+
fiber.keyedHoles = void 0;
|
|
101
|
+
fiber.holes = [];
|
|
102
|
+
};
|
|
103
|
+
const closeFiber = (fiber) => {
|
|
104
|
+
if (fiber.closed) return;
|
|
105
|
+
fiber.closed = true;
|
|
106
|
+
fiber.trackRestore?.();
|
|
107
|
+
fiber.handleError = void 0;
|
|
108
|
+
clearFiberView(fiber);
|
|
109
|
+
Effect.runFork(Scope.close(fiber.scope, Exit.void));
|
|
110
|
+
};
|
|
111
|
+
const makeRuntime = (opts) => {
|
|
112
|
+
const registry = Registry.make();
|
|
113
|
+
const scope = Scope.makeUnsafe();
|
|
114
|
+
let pending = 0;
|
|
115
|
+
let onIdle;
|
|
116
|
+
const runtime = {
|
|
117
|
+
begin: () => {
|
|
118
|
+
pending += 1;
|
|
119
|
+
},
|
|
120
|
+
close: () => {
|
|
121
|
+
onIdle = void 0;
|
|
122
|
+
Effect.runFork(Scope.close(scope, Exit.void));
|
|
123
|
+
registry.dispose();
|
|
124
|
+
},
|
|
125
|
+
end: () => {
|
|
126
|
+
pending -= 1;
|
|
127
|
+
if (pending === 0) onIdle?.();
|
|
128
|
+
},
|
|
129
|
+
fork: Effect.runSyncWith(Context.make(Registry.AtomRegistry, registry)),
|
|
130
|
+
holeSeq: 0,
|
|
131
|
+
hydrateI: 0,
|
|
132
|
+
hydrateValues: opts?.hydrate,
|
|
133
|
+
later: (fn) => {
|
|
134
|
+
runtime.begin();
|
|
135
|
+
queueMicrotask(() => {
|
|
136
|
+
try {
|
|
137
|
+
fn();
|
|
138
|
+
} finally {
|
|
139
|
+
runtime.end();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
onError: opts?.onError,
|
|
144
|
+
registry,
|
|
145
|
+
scope,
|
|
146
|
+
setIdle: (onReady) => {
|
|
147
|
+
onIdle = onReady;
|
|
148
|
+
if (pending === 0) onReady();
|
|
149
|
+
},
|
|
150
|
+
ssr: opts?.ssr === true,
|
|
151
|
+
ssrValues: []
|
|
152
|
+
};
|
|
153
|
+
return runtime;
|
|
154
|
+
};
|
|
155
|
+
/** Walk this fiber and ancestors for an error boundary handler. */
|
|
156
|
+
const reportFiberError = (fiber, error) => {
|
|
157
|
+
for (let f = fiber; f; f = f.parent) if (f.handleError) {
|
|
158
|
+
f.handleError(error);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Open a fiber. A failure reports to the nearest `ErrorBoundary`; uncaught,
|
|
165
|
+
* it calls `runtime.onError`, paints the error view, then `onFail`.
|
|
166
|
+
*/
|
|
167
|
+
const makeFiber = (runtime, root, paint, opts) => {
|
|
168
|
+
const fiber = {
|
|
169
|
+
closed: false,
|
|
170
|
+
fail: (e) => {
|
|
171
|
+
const error = toError(e);
|
|
172
|
+
if (reportFiberError(fiber, error)) return;
|
|
173
|
+
runtime.onError?.(error);
|
|
174
|
+
console.error(error);
|
|
175
|
+
fiber.paint(errorView(error));
|
|
176
|
+
opts?.onFail?.();
|
|
177
|
+
},
|
|
178
|
+
holes: [],
|
|
179
|
+
inFlight: false,
|
|
180
|
+
paint: (view) => paint(fiber, view),
|
|
181
|
+
paintFn: paint,
|
|
182
|
+
parent: opts?.parent,
|
|
183
|
+
registry: runtime.registry,
|
|
184
|
+
root,
|
|
185
|
+
run: (effect, onOk, onErr) => {
|
|
186
|
+
if (fiber.closed) return noop;
|
|
187
|
+
if (onOk) runtime.begin();
|
|
188
|
+
const child = runtime.fork(Effect.forkIn(effect.pipe(Effect.onExit((exit) => Effect.sync(() => {
|
|
189
|
+
if (onOk) runtime.end();
|
|
190
|
+
if (fiber.closed) return;
|
|
191
|
+
if (Exit.isSuccess(exit)) {
|
|
192
|
+
if (onOk) withFiber(fiber, () => onOk(exit.value));
|
|
193
|
+
} else if (onErr && !Cause.hasInterruptsOnly(exit.cause)) withFiber(fiber, () => onErr(Cause.squash(exit.cause)));
|
|
194
|
+
}))), fiber.scope, { startImmediately: true }));
|
|
195
|
+
return () => child.interruptUnsafe();
|
|
196
|
+
},
|
|
197
|
+
runtime,
|
|
198
|
+
scope: Scope.forkUnsafe(runtime.scope)
|
|
199
|
+
};
|
|
200
|
+
return fiber;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/atom.ts
|
|
205
|
+
/** Wrap an effect as a `yield*`-able, awaitable instruction bound to the active fiber. */
|
|
206
|
+
const instr = (effect) => {
|
|
207
|
+
const fiber = getFiber();
|
|
208
|
+
const out = {
|
|
209
|
+
$$ilhaOp: 1,
|
|
210
|
+
effect,
|
|
211
|
+
*[Symbol.iterator]() {
|
|
212
|
+
return yield out;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
Object.defineProperty(out, "then", { value: async (onOk, onErr) => {
|
|
216
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
217
|
+
fiber.run(effect, resolve, reject);
|
|
218
|
+
let value;
|
|
219
|
+
try {
|
|
220
|
+
value = await promise;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (onErr) return onErr(error);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
return onOk ? onOk(value) : value;
|
|
226
|
+
} });
|
|
227
|
+
return out;
|
|
228
|
+
};
|
|
229
|
+
const isInstruction = (x) => hasBrand(x, "$$ilhaOp");
|
|
230
|
+
const isAtomHandle = (x) => hasBrand(x, "$$atom");
|
|
231
|
+
const frames = [];
|
|
232
|
+
let tracking = false;
|
|
233
|
+
/** Read atoms inside `fn` without subscribing the active render. Sync only. */
|
|
234
|
+
const untrack = (fn) => {
|
|
235
|
+
const prev = tracking;
|
|
236
|
+
tracking = false;
|
|
237
|
+
try {
|
|
238
|
+
return fn();
|
|
239
|
+
} finally {
|
|
240
|
+
tracking = prev;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const readTracked = (atom, registry) => {
|
|
244
|
+
if (!tracking) return registry.get(atom);
|
|
245
|
+
const active = getActiveFiber();
|
|
246
|
+
const frame = active ? frames.findLast((f) => f.fiber === active) : frames.at(-1);
|
|
247
|
+
return frame ? frame.get(atom) : registry.get(atom);
|
|
248
|
+
};
|
|
249
|
+
/** Reset per-render slot cursors; warn when the primitive count drifts. */
|
|
250
|
+
const beginPrimitiveFrame = (fiber) => {
|
|
251
|
+
const used = fiber.primitiveI ?? 0;
|
|
252
|
+
const slots = fiber.primitives?.length ?? 0;
|
|
253
|
+
if (used > 0 && used !== slots) ilhaWarn("ILHA002", `${slots} → ${used}`);
|
|
254
|
+
fiber.primitiveI = 0;
|
|
255
|
+
fiber.watchI = 0;
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Run a render with atom reads tracked into `deps`. An async render keeps its
|
|
259
|
+
* frame open until it settles, so reads after `await` still subscribe.
|
|
260
|
+
*/
|
|
261
|
+
const withTrackGetRun = (fiber, fn, onOk, onErr) => {
|
|
262
|
+
const deps = /* @__PURE__ */ new Set();
|
|
263
|
+
const frame = {
|
|
264
|
+
fiber,
|
|
265
|
+
get: (a) => {
|
|
266
|
+
deps.add(a);
|
|
267
|
+
return fiber.registry.get(a);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const restore = () => {
|
|
271
|
+
if (fiber.trackRestore !== restore) return;
|
|
272
|
+
frames.splice(frames.lastIndexOf(frame), 1);
|
|
273
|
+
tracking = frames.length > 0;
|
|
274
|
+
fiber.trackRestore = void 0;
|
|
275
|
+
};
|
|
276
|
+
frames.push(frame);
|
|
277
|
+
tracking = true;
|
|
278
|
+
fiber.trackRestore = restore;
|
|
279
|
+
const done = (value) => {
|
|
280
|
+
restore();
|
|
281
|
+
onOk({
|
|
282
|
+
deps,
|
|
283
|
+
value
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
const failed = (failure) => {
|
|
287
|
+
restore();
|
|
288
|
+
onErr(toError(failure));
|
|
289
|
+
};
|
|
290
|
+
try {
|
|
291
|
+
const result = withFiber(fiber, fn);
|
|
292
|
+
if (result instanceof Promise) {
|
|
293
|
+
const settle = async () => {
|
|
294
|
+
try {
|
|
295
|
+
const value = await result;
|
|
296
|
+
if (!fiber.closed) done(value);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (!fiber.closed) failed(error);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
settle();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
done(result);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
failed(error);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
/** Rerun `run` once per microtask when any of `deps` changes. */
|
|
310
|
+
const subscribeRenderDeps = (fiber, deps, run) => {
|
|
311
|
+
let scheduled = false;
|
|
312
|
+
const schedule = () => {
|
|
313
|
+
if (scheduled) return;
|
|
314
|
+
scheduled = true;
|
|
315
|
+
const seen = fiber.renderGen;
|
|
316
|
+
fiber.runtime.later(() => {
|
|
317
|
+
scheduled = false;
|
|
318
|
+
if (fiber.renderGen === seen) run();
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
const unsubs = [...deps].map((dep) => fiber.registry.subscribe(dep, schedule));
|
|
322
|
+
return () => {
|
|
323
|
+
for (const unsub of unsubs) unsub();
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
/** Callable handle over `atom` that reads through the active render's tracking. */
|
|
327
|
+
const wrapHandle = (atom, fiber) => {
|
|
328
|
+
const { registry } = fiber;
|
|
329
|
+
const guard = () => {
|
|
330
|
+
const active = getActiveFiber();
|
|
331
|
+
if (active && active.registry !== registry) throw ilhaError("ILHA003");
|
|
332
|
+
};
|
|
333
|
+
return Object.assign(() => {
|
|
334
|
+
guard();
|
|
335
|
+
return readTracked(atom, registry);
|
|
336
|
+
}, {
|
|
337
|
+
$$atom: 1,
|
|
338
|
+
atom,
|
|
339
|
+
set: (next) => {
|
|
340
|
+
guard();
|
|
341
|
+
if (Atom.isWritable(atom)) registry.set(atom, next);
|
|
342
|
+
},
|
|
343
|
+
update: (f) => {
|
|
344
|
+
guard();
|
|
345
|
+
if (Atom.isWritable(atom)) registry.update(atom, f);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Claim the fiber's next primitive slot. A fresh slot is built by `make`,
|
|
351
|
+
* seeded from the hydration snapshot when `snapshotted`; SSR records its
|
|
352
|
+
* value so hydration can restore it in the same order.
|
|
353
|
+
*/
|
|
354
|
+
const useSlot = (snapshotted, make) => {
|
|
355
|
+
const fiber = getFiber();
|
|
356
|
+
const { runtime } = fiber;
|
|
357
|
+
const i = fiber.primitiveI ?? 0;
|
|
358
|
+
fiber.primitiveI = i + 1;
|
|
359
|
+
fiber.primitives ??= [];
|
|
360
|
+
let a = fiber.primitives[i];
|
|
361
|
+
if (!a) {
|
|
362
|
+
let hydrated;
|
|
363
|
+
if (snapshotted && runtime.hydrateValues) {
|
|
364
|
+
hydrated = runtime.hydrateValues[runtime.hydrateI];
|
|
365
|
+
runtime.hydrateI += 1;
|
|
366
|
+
}
|
|
367
|
+
a = make(fiber, hydrated);
|
|
368
|
+
fiber.primitives[i] = a;
|
|
369
|
+
if (snapshotted && runtime.ssr) runtime.ssrValues.push(fiber.registry.get(a));
|
|
370
|
+
}
|
|
371
|
+
return wrapHandle(a, fiber);
|
|
372
|
+
};
|
|
373
|
+
/**
|
|
374
|
+
* Keep a component's own state atom in the registry while its fiber lives.
|
|
375
|
+
* The registry drops an atom nothing subscribes to, so state a handler
|
|
376
|
+
* writes and the view never reads would otherwise reset between events.
|
|
377
|
+
*/
|
|
378
|
+
const holdState = (fiber, atom) => {
|
|
379
|
+
if (!fiber.runtime.ssr) {
|
|
380
|
+
fiber.primitiveHolds ??= [];
|
|
381
|
+
fiber.primitiveHolds.push(fiber.registry.mount(atom));
|
|
382
|
+
}
|
|
383
|
+
return atom;
|
|
384
|
+
};
|
|
385
|
+
const atom = Object.assign((init, options) => {
|
|
386
|
+
const plain = !Atom.isAtom(init) && !Effect.isEffect(init) && !Stream.isStream(init);
|
|
387
|
+
return useSlot(plain && options?.snapshot !== false, (fiber, seed) => {
|
|
388
|
+
if (Atom.isAtom(init)) return init;
|
|
389
|
+
if (!plain) return Atom.make(init);
|
|
390
|
+
const made = Atom.make(seed === void 0 ? init : seed);
|
|
391
|
+
const eq = options?.equals;
|
|
392
|
+
return holdState(fiber, eq ? Atom.withEquality(made, eq === "structural" ? Equal.equals : eq) : made);
|
|
393
|
+
});
|
|
394
|
+
}, { lazy: (init, options) => useSlot(options?.snapshot !== false, (fiber, seed) => holdState(fiber, Atom.make(seed === void 0 ? init() : seed))) });
|
|
395
|
+
const { batch } = Atom;
|
|
396
|
+
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/interpret.ts
|
|
399
|
+
/** Drive a generator component: paint yielded views, run yielded effects. */
|
|
400
|
+
const interpret = (gen, fiber) => {
|
|
401
|
+
const iter = gen();
|
|
402
|
+
const resume = (send, thrown) => {
|
|
403
|
+
if (fiber.closed) return;
|
|
404
|
+
withFiber(fiber, () => {
|
|
405
|
+
if (fiber.inFlight) throw ilhaError("ILHA004");
|
|
406
|
+
let step;
|
|
407
|
+
try {
|
|
408
|
+
step = thrown ? iter.throw(thrown) : iter.next(send);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
fiber.fail(error);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const { value } = step;
|
|
414
|
+
if (step.done) {
|
|
415
|
+
if (value !== void 0) fiber.paint(value);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const effect = isInstruction(value) ? value.effect : value;
|
|
419
|
+
if (Effect.isEffect(effect)) {
|
|
420
|
+
fiber.inFlight = true;
|
|
421
|
+
fiber.run(effect, (a) => {
|
|
422
|
+
fiber.inFlight = false;
|
|
423
|
+
resume(a);
|
|
424
|
+
}, (e) => {
|
|
425
|
+
fiber.inFlight = false;
|
|
426
|
+
resume(void 0, toError(e));
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
fiber.paint(value);
|
|
431
|
+
resume();
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
resume();
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/start.ts
|
|
439
|
+
/** Render `fn` into `fiber` and rerender when an atom it read changes. */
|
|
440
|
+
const runSetup = (fiber, fn) => {
|
|
441
|
+
const render = () => {
|
|
442
|
+
if (fiber.closed) return;
|
|
443
|
+
fiber.renderGen = (fiber.renderGen ?? 0) + 1;
|
|
444
|
+
fiber.runtime.begin();
|
|
445
|
+
beginPrimitiveFrame(fiber);
|
|
446
|
+
withTrackGetRun(fiber, fn, ({ value, deps }) => {
|
|
447
|
+
try {
|
|
448
|
+
fiber.renderSub?.();
|
|
449
|
+
fiber.renderSub = void 0;
|
|
450
|
+
const gen = value;
|
|
451
|
+
if (isFunction(gen?.next) && isFunction(gen?.throw)) {
|
|
452
|
+
interpret(() => gen, fiber);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (!fiber.runtime.ssr) fiber.renderSub = subscribeRenderDeps(fiber, deps, render);
|
|
456
|
+
if (value !== void 0 && !fiber.closed) withFiber(fiber, () => fiber.paint(value));
|
|
457
|
+
} catch (error) {
|
|
458
|
+
withFiber(fiber, () => fiber.fail(error));
|
|
459
|
+
} finally {
|
|
460
|
+
fiber.runtime.end();
|
|
461
|
+
}
|
|
462
|
+
}, (failure) => {
|
|
463
|
+
withFiber(fiber, () => fiber.fail(failure));
|
|
464
|
+
fiber.runtime.end();
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
try {
|
|
468
|
+
render();
|
|
469
|
+
} catch (error) {
|
|
470
|
+
fiber.fail(error);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
/** Live root fibers, for `ilha/debug`. Costs one set add/delete per root. */
|
|
474
|
+
const liveRoots = /* @__PURE__ */ new Set();
|
|
475
|
+
/** Called after a paint, a root settling, or a root closing. `ilha/debug` fills it. */
|
|
476
|
+
const rootListeners = /* @__PURE__ */ new Set();
|
|
477
|
+
const notifyRoots = () => {
|
|
478
|
+
for (const listener of rootListeners) listener();
|
|
479
|
+
};
|
|
480
|
+
/** Open a runtime and root fiber, then run `fn` as the root component. */
|
|
481
|
+
const startRoot = ({ fn, root, paint, ssr, hydrate, snapshot, onError, onIdle, onRuntime }) => {
|
|
482
|
+
const runtime = makeRuntime({
|
|
483
|
+
hydrate: snapshot,
|
|
484
|
+
onError,
|
|
485
|
+
ssr
|
|
486
|
+
});
|
|
487
|
+
onRuntime?.(runtime);
|
|
488
|
+
const { promise: ready, resolve } = Promise.withResolvers();
|
|
489
|
+
const settle = () => resolve(null);
|
|
490
|
+
const fiber = makeFiber(runtime, root, paint, { onFail: settle });
|
|
491
|
+
fiber.hydrate = hydrate;
|
|
492
|
+
fiber.componentType = fn;
|
|
493
|
+
liveRoots.add(fiber);
|
|
494
|
+
runtime.begin();
|
|
495
|
+
runtime.setIdle(() => {
|
|
496
|
+
settle();
|
|
497
|
+
onIdle?.();
|
|
498
|
+
notifyRoots();
|
|
499
|
+
});
|
|
500
|
+
runSetup(fiber, fn);
|
|
501
|
+
runtime.end();
|
|
502
|
+
return {
|
|
503
|
+
close: () => {
|
|
504
|
+
closeFiber(fiber);
|
|
505
|
+
runtime.close();
|
|
506
|
+
if (liveRoots.delete(fiber)) notifyRoots();
|
|
507
|
+
},
|
|
508
|
+
fiber,
|
|
509
|
+
ready,
|
|
510
|
+
runtime
|
|
511
|
+
};
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
//#endregion
|
|
515
|
+
export { IlhaError as _, startRoot as a, batch as c, untrack as d, wrapHandle as f, makeFiber as g, getFiber as h, runSetup as i, instr as l, closeFiber as m, notifyRoots as n, interpret as o, clearFiberView as p, rootListeners as r, atom as s, liveRoots as t, isAtomHandle as u, ilhaError as v, ilhaWarn as y };
|