fibrae 0.1.0 → 0.1.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/dist/components.d.ts +45 -11
- package/dist/components.js +80 -18
- package/dist/components.js.map +1 -1
- package/dist/core.js.map +1 -1
- package/dist/dom.d.ts +4 -1
- package/dist/dom.js +15 -2
- package/dist/dom.js.map +1 -1
- package/dist/fiber-render.d.ts +5 -3
- package/dist/fiber-render.js +197 -114
- package/dist/fiber-render.js.map +1 -1
- package/dist/h.d.ts +5 -10
- package/dist/h.js +9 -3
- package/dist/h.js.map +1 -1
- package/dist/hydration.js +34 -48
- package/dist/hydration.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime/index.d.ts +6 -5
- package/dist/jsx-runtime/index.js +4 -4
- package/dist/jsx-runtime/index.js.map +1 -1
- package/dist/render.js +20 -60
- package/dist/render.js.map +1 -1
- package/dist/router/History.js.map +1 -1
- package/dist/router/Link.d.ts +2 -2
- package/dist/router/Link.js +8 -4
- package/dist/router/Link.js.map +1 -1
- package/dist/router/Navigator.js +1 -3
- package/dist/router/Navigator.js.map +1 -1
- package/dist/router/Route.js +1 -1
- package/dist/router/Route.js.map +1 -1
- package/dist/router/Router.js +1 -3
- package/dist/router/Router.js.map +1 -1
- package/dist/router/RouterBuilder.js.map +1 -1
- package/dist/router/RouterState.js.map +1 -1
- package/dist/runtime.js +9 -2
- package/dist/runtime.js.map +1 -1
- package/dist/scope-utils.js.map +1 -1
- package/dist/server.js +16 -23
- package/dist/server.js.map +1 -1
- package/dist/shared.d.ts +85 -10
- package/dist/shared.js +34 -3
- package/dist/shared.js.map +1 -1
- package/dist/tracking.js +1 -1
- package/package.json +27 -23
package/dist/fiber-render.js
CHANGED
|
@@ -20,11 +20,10 @@ import * as Context from "effect/Context";
|
|
|
20
20
|
import * as FiberRef from "effect/FiberRef";
|
|
21
21
|
import * as Cause from "effect/Cause";
|
|
22
22
|
import { Atom, Registry as AtomRegistry } from "@effect-atom/atom";
|
|
23
|
-
import { isEvent, isProperty, isStream, } from "./shared.js";
|
|
23
|
+
import { RenderError, StreamError, EventHandlerError, isEvent, isProperty, isStream, } from "./shared.js";
|
|
24
24
|
import { FibraeRuntime, runForkWithRuntime } from "./runtime.js";
|
|
25
25
|
import { setDomProperty, attachEventListeners } from "./dom.js";
|
|
26
26
|
import { normalizeToStream, makeTrackingRegistry } from "./tracking.js";
|
|
27
|
-
import { h } from "./h.js";
|
|
28
27
|
// =============================================================================
|
|
29
28
|
// Stream Subscription Helper
|
|
30
29
|
// =============================================================================
|
|
@@ -56,11 +55,17 @@ const subscribeComponentStream = (stream, fiberRef, scope) => Effect.gen(functio
|
|
|
56
55
|
}
|
|
57
56
|
})).pipe(Effect.catchAllCause((cause) => Effect.gen(function* () {
|
|
58
57
|
const done = yield* Deferred.isDone(firstValueDeferred);
|
|
58
|
+
// Convert to StreamError with the correct phase
|
|
59
|
+
const streamError = new StreamError({
|
|
60
|
+
cause: Cause.squash(cause),
|
|
61
|
+
phase: done ? "after-first-emission" : "before-first-emission",
|
|
62
|
+
});
|
|
59
63
|
if (!done) {
|
|
60
|
-
|
|
64
|
+
// Fail the deferred with StreamError so awaiting code gets the typed error
|
|
65
|
+
yield* Deferred.fail(firstValueDeferred, streamError);
|
|
61
66
|
}
|
|
62
67
|
const currentFiber = fiberRef.current;
|
|
63
|
-
yield* handleFiberError(currentFiber,
|
|
68
|
+
yield* handleFiberError(currentFiber, streamError);
|
|
64
69
|
})));
|
|
65
70
|
yield* Effect.forkIn(subscription, scope);
|
|
66
71
|
return firstValueDeferred;
|
|
@@ -83,7 +88,7 @@ const createFiber = (type, props, parent, alternate, effectTag) => ({
|
|
|
83
88
|
childFirstCommitDeferred: Option.none(),
|
|
84
89
|
fiberRef: Option.none(),
|
|
85
90
|
isMultiEmissionStream: false,
|
|
86
|
-
|
|
91
|
+
boundary: Option.none(),
|
|
87
92
|
suspense: Option.none(),
|
|
88
93
|
renderContext: Option.none(),
|
|
89
94
|
isParked: false,
|
|
@@ -101,8 +106,7 @@ const fiberTypeIsFunction = (fiber) => fiber.type.pipe(Option.map((t) => typeof
|
|
|
101
106
|
* Check if a fiber is a virtual element (no DOM node created).
|
|
102
107
|
* Returns true for root fiber (no type) or FRAGMENT type.
|
|
103
108
|
*/
|
|
104
|
-
const fiberIsVirtualElement = (fiber) => fiber.type.pipe(Option.map((t) => t === "FRAGMENT"), Option.getOrElse(() => true)
|
|
105
|
-
);
|
|
109
|
+
const fiberIsVirtualElement = (fiber) => fiber.type.pipe(Option.map((t) => t === "FRAGMENT"), Option.getOrElse(() => true));
|
|
106
110
|
/**
|
|
107
111
|
* Get a fiber's required componentScope or die with a message.
|
|
108
112
|
*/
|
|
@@ -189,19 +193,6 @@ const processBatch = () => Effect.gen(function* () {
|
|
|
189
193
|
// =============================================================================
|
|
190
194
|
// Fiber Tree Walking Helpers
|
|
191
195
|
// =============================================================================
|
|
192
|
-
/**
|
|
193
|
-
* Walk up the fiber tree from the starting fiber, returning the first ancestor
|
|
194
|
-
* (including the starting fiber) that matches the predicate.
|
|
195
|
-
*/
|
|
196
|
-
const findAncestor = (fiber, predicate) => {
|
|
197
|
-
let current = Option.some(fiber);
|
|
198
|
-
while (Option.isSome(current)) {
|
|
199
|
-
if (predicate(current.value))
|
|
200
|
-
return current;
|
|
201
|
-
current = current.value.parent;
|
|
202
|
-
}
|
|
203
|
-
return Option.none();
|
|
204
|
-
};
|
|
205
196
|
/**
|
|
206
197
|
* Walk up the fiber tree from the starting fiber's parent, returning the first
|
|
207
198
|
* ancestor that matches the predicate (excludes the starting fiber itself).
|
|
@@ -239,40 +230,55 @@ const findDomParent = (fiber) => {
|
|
|
239
230
|
// =============================================================================
|
|
240
231
|
// Error Boundary Support
|
|
241
232
|
// =============================================================================
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
cfg.onError?.(cause);
|
|
256
|
-
cfg.hasError = true;
|
|
257
|
-
boundary.errorBoundary = Option.some(cfg);
|
|
258
|
-
// Check if we're in initial render (no currentRoot yet)
|
|
259
|
-
if (Option.isNone(state.currentRoot)) {
|
|
260
|
-
// During initial render: re-reconcile boundary with fallback immediately
|
|
261
|
-
// This replaces the errored child with the fallback element
|
|
262
|
-
yield* reconcileChildren(boundary, [cfg.fallback]);
|
|
263
|
-
// Return the boundary's new child (fallback) so work continues
|
|
264
|
-
return boundary.child;
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
// After initial render: queue for re-render
|
|
268
|
-
yield* queueFiberForRerender(boundary);
|
|
269
|
-
return Option.none();
|
|
270
|
-
}
|
|
233
|
+
/** Find nearest BOUNDARY by walking up the fiber tree */
|
|
234
|
+
const findNearestBoundary = (fiber) => findAncestorExcludingSelf(fiber, (f) => Option.isSome(f.boundary));
|
|
235
|
+
/**
|
|
236
|
+
* Convert a raw cause/error to a ComponentError.
|
|
237
|
+
* If it's already a ComponentError, return as-is.
|
|
238
|
+
* Otherwise, wrap in RenderError as a fallback.
|
|
239
|
+
*/
|
|
240
|
+
const toComponentError = (cause) => {
|
|
241
|
+
// Check if it's already a ComponentError
|
|
242
|
+
if (cause instanceof RenderError ||
|
|
243
|
+
cause instanceof StreamError ||
|
|
244
|
+
cause instanceof EventHandlerError) {
|
|
245
|
+
return cause;
|
|
271
246
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
247
|
+
// Extract the actual error from Cause if needed
|
|
248
|
+
const actualError = Cause.isCause(cause) ? Cause.squash(cause) : cause;
|
|
249
|
+
if (actualError instanceof RenderError ||
|
|
250
|
+
actualError instanceof StreamError ||
|
|
251
|
+
actualError instanceof EventHandlerError) {
|
|
252
|
+
return actualError;
|
|
275
253
|
}
|
|
254
|
+
// Wrap unknown errors as RenderError
|
|
255
|
+
return new RenderError({ cause: actualError });
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Handle an error by finding the nearest error boundary and invoking its handler.
|
|
259
|
+
*
|
|
260
|
+
* Finds the nearest BOUNDARY and calls its onError callback to fail the stream.
|
|
261
|
+
* The Stream.catchTags in user code will produce the fallback.
|
|
262
|
+
*/
|
|
263
|
+
const handleFiberError = (fiber, cause) => Effect.gen(function* () {
|
|
264
|
+
const componentError = toComponentError(cause);
|
|
265
|
+
// Find nearest boundary
|
|
266
|
+
const boundaryOpt = findNearestBoundary(fiber);
|
|
267
|
+
return yield* Option.match(boundaryOpt, {
|
|
268
|
+
onNone: () => Effect.gen(function* () {
|
|
269
|
+
yield* Effect.logError("Unhandled error without any error boundary", componentError);
|
|
270
|
+
return Option.none();
|
|
271
|
+
}),
|
|
272
|
+
onSome: (boundary) => Effect.sync(() => {
|
|
273
|
+
// Call onError to fail the stream
|
|
274
|
+
const cfg = Option.getOrThrow(boundary.boundary);
|
|
275
|
+
cfg.onError(componentError);
|
|
276
|
+
cfg.hasError = true;
|
|
277
|
+
// The stream will fail and catchTags will produce fallback
|
|
278
|
+
// which will trigger a re-render via the stream subscription
|
|
279
|
+
return Option.none();
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
276
282
|
});
|
|
277
283
|
// =============================================================================
|
|
278
284
|
// Suspense Support
|
|
@@ -390,14 +396,14 @@ const updateFunctionComponent = (fiber, runtime) => Effect.gen(function* () {
|
|
|
390
396
|
return Effect.sync(() => component(fiber.props));
|
|
391
397
|
},
|
|
392
398
|
});
|
|
393
|
-
// Fast path: if component returns a plain VElement (not Effect/Stream),
|
|
399
|
+
// Fast path: if component returns a plain VElement (not Effect/Stream),
|
|
394
400
|
// and it's a special element type, just reconcile directly without stream machinery.
|
|
395
401
|
// This handles wrapper components like Suspense and ErrorBoundary efficiently.
|
|
396
402
|
if (!Effect.isEffect(output) && !isStream(output)) {
|
|
397
403
|
const vElement = output;
|
|
398
404
|
if (typeof vElement === "object" && vElement !== null && "type" in vElement) {
|
|
399
405
|
const elementType = vElement.type;
|
|
400
|
-
if (elementType === "SUSPENSE" || elementType === "
|
|
406
|
+
if (elementType === "SUSPENSE" || elementType === "FRAGMENT") {
|
|
401
407
|
// Simple wrapper component - just reconcile with the VElement directly
|
|
402
408
|
yield* reconcileChildren(fiber, [vElement]);
|
|
403
409
|
return;
|
|
@@ -509,29 +515,31 @@ const updateHostComponent = (fiber, runtime) => Effect.gen(function* () {
|
|
|
509
515
|
if (Option.isNone(fiber.renderContext) && Option.isSome(fiber.parent)) {
|
|
510
516
|
fiber.renderContext = fiber.parent.value.renderContext;
|
|
511
517
|
}
|
|
512
|
-
// Handle
|
|
513
|
-
const
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
518
|
+
// Handle BOUNDARY specially (Effect-native error boundary API)
|
|
519
|
+
const isBoundary = fiberTypeIs(fiber, "BOUNDARY");
|
|
520
|
+
if (isBoundary) {
|
|
521
|
+
const boundaryId = fiber.props.boundaryId;
|
|
522
|
+
const onError = fiber.props.onError;
|
|
523
|
+
// Inherit boundary state from alternate (previous render) if present
|
|
524
|
+
// This preserves hasError state across re-renders
|
|
525
|
+
if (Option.isNone(fiber.boundary) && Option.isSome(fiber.alternate)) {
|
|
526
|
+
const alt = fiber.alternate.value;
|
|
527
|
+
if (Option.isSome(alt.boundary)) {
|
|
528
|
+
fiber.boundary = alt.boundary;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Initialize boundary config if not already set
|
|
532
|
+
if (Option.isNone(fiber.boundary)) {
|
|
533
|
+
fiber.boundary = Option.some({
|
|
534
|
+
boundaryId,
|
|
521
535
|
onError,
|
|
522
536
|
hasError: false,
|
|
523
537
|
});
|
|
524
538
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
// Normal state - render children
|
|
532
|
-
const children = fiber.props.children;
|
|
533
|
-
yield* reconcileChildren(fiber, children || []);
|
|
534
|
-
}
|
|
539
|
+
// Render children (BOUNDARY doesn't have fallback state -
|
|
540
|
+
// the fallback is handled by Stream.catchTags in userland)
|
|
541
|
+
const children = fiber.props.children;
|
|
542
|
+
yield* reconcileChildren(fiber, children || []);
|
|
535
543
|
return;
|
|
536
544
|
}
|
|
537
545
|
// Handle SUSPENSE specially
|
|
@@ -614,9 +622,7 @@ const createDom = (fiber, runtime) => Effect.gen(function* () {
|
|
|
614
622
|
if (typeof type !== "string") {
|
|
615
623
|
return Effect.die("createDom called on function component");
|
|
616
624
|
}
|
|
617
|
-
const node = type === "TEXT_ELEMENT"
|
|
618
|
-
? document.createTextNode("")
|
|
619
|
-
: document.createElement(type);
|
|
625
|
+
const node = type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(type);
|
|
620
626
|
return Effect.succeed(node);
|
|
621
627
|
},
|
|
622
628
|
});
|
|
@@ -642,9 +648,8 @@ const updateDom = (dom, prevProps, nextProps, ownerFiber, runtime) => Effect.gen
|
|
|
642
648
|
if (element instanceof Text) {
|
|
643
649
|
if (nextProps.nodeValue !== prevProps.nodeValue) {
|
|
644
650
|
const value = nextProps.nodeValue;
|
|
645
|
-
element.nodeValue =
|
|
646
|
-
? String(value)
|
|
647
|
-
: "";
|
|
651
|
+
element.nodeValue =
|
|
652
|
+
typeof value === "string" || typeof value === "number" ? String(value) : "";
|
|
648
653
|
}
|
|
649
654
|
return;
|
|
650
655
|
}
|
|
@@ -684,7 +689,14 @@ const updateDom = (dom, prevProps, nextProps, ownerFiber, runtime) => Effect.gen
|
|
|
684
689
|
if (Effect.isEffect(result)) {
|
|
685
690
|
// Use runForkWithRuntime to get the full application context
|
|
686
691
|
// This provides Navigator, FibraeRuntime, AtomRegistry, etc.
|
|
687
|
-
const effectWithErrorHandling = result.pipe(Effect.catchAllCause((cause) =>
|
|
692
|
+
const effectWithErrorHandling = result.pipe(Effect.catchAllCause((cause) => {
|
|
693
|
+
// Convert to EventHandlerError with the event type
|
|
694
|
+
const error = new EventHandlerError({
|
|
695
|
+
cause: Cause.squash(cause),
|
|
696
|
+
eventType,
|
|
697
|
+
});
|
|
698
|
+
return ownerFiber ? handleFiberError(ownerFiber, error) : Effect.void;
|
|
699
|
+
}));
|
|
688
700
|
runForkWithRuntime(runtime)(effectWithErrorHandling);
|
|
689
701
|
}
|
|
690
702
|
};
|
|
@@ -762,7 +774,7 @@ const reconcileChildren = (wipFiber, elements) => Effect.gen(function* () {
|
|
|
762
774
|
// UPDATE - reuse DOM, update props
|
|
763
775
|
const fiber = createFiber(matched.type, element.props, Option.some(wipFiber), matchedOldOpt, Option.some("UPDATE"));
|
|
764
776
|
fiber.dom = matched.dom;
|
|
765
|
-
fiber.
|
|
777
|
+
fiber.boundary = matched.boundary;
|
|
766
778
|
fiber.suspense = matched.suspense;
|
|
767
779
|
return fiber;
|
|
768
780
|
}
|
|
@@ -959,11 +971,62 @@ export const renderFiber = (element, container) => Effect.gen(function* () {
|
|
|
959
971
|
// =============================================================================
|
|
960
972
|
// Hydration Support
|
|
961
973
|
// =============================================================================
|
|
974
|
+
// =============================================================================
|
|
975
|
+
// Hydration Helpers - Cursor-based DOM Walking (like React)
|
|
976
|
+
// =============================================================================
|
|
977
|
+
/**
|
|
978
|
+
* Get the next hydratable node, skipping whitespace-only text nodes
|
|
979
|
+
* and non-marker comments. Returns None if no more hydratable nodes.
|
|
980
|
+
*
|
|
981
|
+
* Hydratable nodes are:
|
|
982
|
+
* - Element nodes (always)
|
|
983
|
+
* - Text nodes with non-whitespace content
|
|
984
|
+
* - Comment nodes that are Fibrae markers (fibrae:...)
|
|
985
|
+
*/
|
|
986
|
+
const getNextHydratable = (node) => {
|
|
987
|
+
let current = node;
|
|
988
|
+
while (current) {
|
|
989
|
+
const nodeType = current.nodeType;
|
|
990
|
+
// Element nodes are always hydratable
|
|
991
|
+
if (nodeType === Node.ELEMENT_NODE) {
|
|
992
|
+
return Option.some(current);
|
|
993
|
+
}
|
|
994
|
+
// Text nodes are hydratable if they have non-whitespace content
|
|
995
|
+
if (nodeType === Node.TEXT_NODE) {
|
|
996
|
+
const textContent = current.textContent;
|
|
997
|
+
if (textContent !== null && textContent.trim() !== "") {
|
|
998
|
+
return Option.some(current);
|
|
999
|
+
}
|
|
1000
|
+
// Skip whitespace-only text nodes
|
|
1001
|
+
}
|
|
1002
|
+
// Comment nodes are hydratable if they're Fibrae markers
|
|
1003
|
+
if (nodeType === Node.COMMENT_NODE) {
|
|
1004
|
+
const data = current.data;
|
|
1005
|
+
if (data.startsWith("fibrae:")) {
|
|
1006
|
+
return Option.some(current);
|
|
1007
|
+
}
|
|
1008
|
+
// Skip non-marker comments
|
|
1009
|
+
}
|
|
1010
|
+
current = current.nextSibling;
|
|
1011
|
+
}
|
|
1012
|
+
return Option.none();
|
|
1013
|
+
};
|
|
1014
|
+
const getFirstHydratableChild = (parent) => {
|
|
1015
|
+
return getNextHydratable(parent.firstChild);
|
|
1016
|
+
};
|
|
1017
|
+
const getNextHydratableSibling = (node) => {
|
|
1018
|
+
return getNextHydratable(node.nextSibling);
|
|
1019
|
+
};
|
|
1020
|
+
// =============================================================================
|
|
1021
|
+
// Fiber Hydration - Cursor-based Implementation
|
|
1022
|
+
// =============================================================================
|
|
962
1023
|
/**
|
|
963
|
-
* Hydrate an existing DOM tree
|
|
1024
|
+
* Hydrate an existing DOM tree by building a fiber tree that references
|
|
1025
|
+
* the existing DOM nodes. This enables event handlers and reactivity
|
|
1026
|
+
* without replacing the DOM.
|
|
964
1027
|
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
1028
|
+
* Uses cursor-based DOM walking (like React) to match VElement tree
|
|
1029
|
+
* against existing DOM, skipping whitespace-only text nodes and comments.
|
|
967
1030
|
*/
|
|
968
1031
|
export const hydrateFiber = (element, container) => Effect.gen(function* () {
|
|
969
1032
|
const runtime = yield* FibraeRuntime;
|
|
@@ -972,8 +1035,9 @@ export const hydrateFiber = (element, container) => Effect.gen(function* () {
|
|
|
972
1035
|
const rootFiber = createFiber(Option.none(), { children: [element] }, Option.none(), Option.none(), Option.none());
|
|
973
1036
|
rootFiber.dom = Option.some(container);
|
|
974
1037
|
// Build fiber tree by walking DOM and VElement together
|
|
975
|
-
//
|
|
976
|
-
|
|
1038
|
+
// Start with first hydratable child (skipping whitespace)
|
|
1039
|
+
const firstChild = getFirstHydratableChild(container);
|
|
1040
|
+
yield* hydrateChildren(rootFiber, [element], firstChild, runtime);
|
|
977
1041
|
yield* Ref.update(stateRef, (s) => ({
|
|
978
1042
|
...s,
|
|
979
1043
|
currentRoot: Option.some(rootFiber),
|
|
@@ -983,59 +1047,76 @@ export const hydrateFiber = (element, container) => Effect.gen(function* () {
|
|
|
983
1047
|
// Keep runtime alive
|
|
984
1048
|
return yield* Effect.never;
|
|
985
1049
|
});
|
|
986
|
-
|
|
987
|
-
|
|
1050
|
+
/**
|
|
1051
|
+
* Hydrate multiple vElements against DOM nodes using cursor-based walking.
|
|
1052
|
+
* Returns the next DOM cursor position after all children are hydrated.
|
|
1053
|
+
*/
|
|
1054
|
+
const hydrateChildren = (parentFiber, vElements, startCursor, runtime) => Effect.gen(function* () {
|
|
1055
|
+
let cursor = startCursor;
|
|
988
1056
|
const fibers = [];
|
|
989
1057
|
for (const vElement of vElements) {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
domIndex++;
|
|
1058
|
+
if (Option.isNone(cursor)) {
|
|
1059
|
+
// No more DOM nodes but we have more vElements - mismatch
|
|
1060
|
+
// For now, just skip (could throw HydrationMismatch in strict mode)
|
|
1061
|
+
break;
|
|
995
1062
|
}
|
|
996
|
-
|
|
1063
|
+
const { fiber, nextCursor } = yield* hydrateElement(parentFiber, vElement, cursor.value, runtime);
|
|
1064
|
+
fibers.push(fiber);
|
|
1065
|
+
cursor = nextCursor;
|
|
997
1066
|
}
|
|
998
1067
|
// Link fibers
|
|
999
1068
|
linkFibersAsSiblings(fibers, parentFiber);
|
|
1069
|
+
return cursor;
|
|
1000
1070
|
});
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1071
|
+
/**
|
|
1072
|
+
* Hydrate a single vElement against a DOM node.
|
|
1073
|
+
* Returns the created fiber and the next DOM cursor position.
|
|
1074
|
+
*/
|
|
1075
|
+
const hydrateElement = (parentFiber, vElement, domNode, runtime) => Effect.gen(function* () {
|
|
1076
|
+
const fiber = createFiber(Option.some(vElement.type), vElement.props, Option.some(parentFiber), Option.none(), Option.none());
|
|
1077
|
+
let nextCursor;
|
|
1004
1078
|
if (typeof vElement.type === "function") {
|
|
1005
1079
|
// Function component - invoke to get children
|
|
1006
|
-
yield* hydrateFunctionComponent(fiber, vElement,
|
|
1080
|
+
nextCursor = yield* hydrateFunctionComponent(fiber, vElement, domNode, runtime);
|
|
1007
1081
|
}
|
|
1008
1082
|
else if (vElement.type === "TEXT_ELEMENT") {
|
|
1009
|
-
// Text node
|
|
1010
|
-
const domNode = domNodes[domIndex];
|
|
1083
|
+
// Text node - adopt the DOM text node
|
|
1011
1084
|
fiber.dom = Option.some(domNode);
|
|
1085
|
+
nextCursor = getNextHydratableSibling(domNode);
|
|
1012
1086
|
}
|
|
1013
1087
|
else if (vElement.type === "FRAGMENT") {
|
|
1014
|
-
// Fragment - children
|
|
1015
|
-
yield* hydrateChildren(fiber, vElement.props.children || [],
|
|
1088
|
+
// Fragment - children consume DOM nodes starting from current cursor
|
|
1089
|
+
nextCursor = yield* hydrateChildren(fiber, vElement.props.children || [], Option.some(domNode), runtime);
|
|
1016
1090
|
}
|
|
1017
1091
|
else {
|
|
1018
|
-
// Host element
|
|
1019
|
-
const
|
|
1020
|
-
fiber.dom = Option.some(
|
|
1092
|
+
// Host element - adopt DOM node and hydrate children
|
|
1093
|
+
const el = domNode;
|
|
1094
|
+
fiber.dom = Option.some(el);
|
|
1021
1095
|
// Inherit renderContext from parent fiber (function components capture it during render)
|
|
1022
1096
|
if (Option.isNone(fiber.renderContext) && Option.isSome(fiber.parent)) {
|
|
1023
1097
|
fiber.renderContext = fiber.parent.value.renderContext;
|
|
1024
1098
|
}
|
|
1025
1099
|
// Attach event listeners - uses runForkWithRuntime internally for full context
|
|
1026
|
-
|
|
1100
|
+
// Pass error callback to trigger ErrorBoundary on event handler failures
|
|
1101
|
+
attachEventListeners(el, vElement.props, runtime, (cause) => handleFiberError(fiber, cause));
|
|
1027
1102
|
// Handle ref
|
|
1028
1103
|
const ref = vElement.props.ref;
|
|
1029
1104
|
if (ref && typeof ref === "object" && "current" in ref) {
|
|
1030
|
-
ref.current =
|
|
1105
|
+
ref.current = el;
|
|
1031
1106
|
}
|
|
1032
|
-
// Hydrate children
|
|
1033
|
-
const
|
|
1034
|
-
yield* hydrateChildren(fiber, vElement.props.children || [],
|
|
1107
|
+
// Hydrate children using cursor-based walking
|
|
1108
|
+
const firstChildCursor = getFirstHydratableChild(el);
|
|
1109
|
+
yield* hydrateChildren(fiber, vElement.props.children || [], firstChildCursor, runtime);
|
|
1110
|
+
// Move to next sibling for the parent's cursor
|
|
1111
|
+
nextCursor = getNextHydratableSibling(domNode);
|
|
1035
1112
|
}
|
|
1036
|
-
return fiber;
|
|
1113
|
+
return { fiber, nextCursor };
|
|
1037
1114
|
});
|
|
1038
|
-
|
|
1115
|
+
/**
|
|
1116
|
+
* Hydrate a function component by invoking it and hydrating its output.
|
|
1117
|
+
* Returns the next DOM cursor position.
|
|
1118
|
+
*/
|
|
1119
|
+
const hydrateFunctionComponent = (fiber, vElement, domNode, runtime) => Effect.gen(function* () {
|
|
1039
1120
|
// Capture current context during render phase for event handlers in commit phase
|
|
1040
1121
|
const currentContext = (yield* FiberRef.get(FiberRef.currentContext));
|
|
1041
1122
|
fiber.renderContext = Option.some(currentContext);
|
|
@@ -1061,9 +1142,11 @@ const hydrateFunctionComponent = (fiber, vElement, domNodes, domIndex, runtime)
|
|
|
1061
1142
|
// Wait for first value
|
|
1062
1143
|
const childVElement = yield* Deferred.await(firstValueDeferred);
|
|
1063
1144
|
fiber.latestStreamValue = Option.some(childVElement);
|
|
1064
|
-
// Hydrate the child VElement against
|
|
1065
|
-
|
|
1145
|
+
// Hydrate the child VElement against DOM node
|
|
1146
|
+
// Function components render output directly to DOM (no wrapper)
|
|
1147
|
+
const nextCursor = yield* hydrateChildren(fiber, [childVElement], Option.some(domNode), runtime);
|
|
1066
1148
|
// Subscribe to atom changes
|
|
1067
1149
|
yield* subscribeFiberAtoms(fiber, accessedAtoms, runtime);
|
|
1150
|
+
return nextCursor;
|
|
1068
1151
|
});
|
|
1069
1152
|
//# sourceMappingURL=fiber-render.js.map
|