Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
95db6ce
Updated CHANGELOG and package.json
hexplus Mar 28, 2026
56080d8
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 28, 2026
7eeec49
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 28, 2026
14a9cd4
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
9487727
ci: use npm install instead of npm ci
hexplus Mar 29, 2026
6b4bd83
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0b9a0cc
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0777184
trusted-publisher
hexplus Mar 29, 2026
4d46e82
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
bea9788
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
825a8dc
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
55c4436
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0d2c7e0
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
8da81e8
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
325ce5d
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0cad329
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 1, 2026
aea6787
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 4, 2026
00e5e88
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 7, 2026
b10a2c5
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 7, 2026
639eae0
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 9, 2026
405e4fe
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 11, 2026
ee7cf48
Updated main
hexplus Apr 11, 2026
8c77fca
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 11, 2026
da6d752
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 11, 2026
c047837
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 12, 2026
a52fffc
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 12, 2026
43b5675
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 14, 2026
44df880
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 18, 2026
aba311a
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 19, 2026
2f603ee
Refactored ErrorBoundary
hexplus Apr 19, 2026
93168c5
Added ErrorBoundary test
hexplus Apr 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,42 @@ This project follows [Semantic Versioning](https://semver.org/).

---

## [3.0.0] — 2026-04-19

### Breaking

- **`ErrorBoundary` drops the `nodes` option** — the subtree is now passed as the positional second argument, matching the tag-factory shorthand (`tag(props, children)`). This removes the last `nodes:` prop from the public framework surface (tag factories migrated in 1.3.0). Signature:

```ts
ErrorBoundary(children: () => Element): Element;
ErrorBoundary(options: ErrorBoundaryOptions, children: () => Element): Element;
```

**Migration:**

```ts
// Before
ErrorBoundary({
nodes: () => RiskyArea(),
fallback: (err, retry) => …,
onError,
resetKeys,
});

// After
ErrorBoundary(
{ fallback: (err, retry) => …, onError, resetKeys },
() => RiskyArea(),
);

// Options-free form
ErrorBoundary(() => RiskyArea());
```

`ErrorBoundaryProps` is retained as a deprecated alias of the renamed `ErrorBoundaryOptions` so type imports keep compiling.

---

## [2.2.0] — 2026-04-18

Reactivity-core rewrite. Replaces the `Set<Subscriber>` / `Map<Signal, epoch>` subscription graph with doubly-linked `SubNode` edges, a node pool, and an `__activeNode` back-pointer for O(1) duplicate-dependency detection. Subscription is now O(1) on both add and remove, the hot path has no hash operations, and GC churn on create/destroy workloads drops sharply.
Expand Down
21 changes: 11 additions & 10 deletions docs/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,18 +268,19 @@ function LiveFeed(): HTMLElement {
### Wrap components with ErrorBoundary

```ts
import { ErrorBoundary } from "sibujs";
import { ErrorBoundary, div, p, button } from "sibujs";

function App(): HTMLElement {
return ErrorBoundary({
nodes: () => MainContent(),
fallback: (err, retry) => div({
nodes: [
p({ nodes: `Error: ${err.message}` }),
button({ nodes: "Retry", on: { click: retry } }),
],
}) as HTMLElement,
});
return ErrorBoundary(
{
fallback: (err, retry) =>
div([
p(`Error: ${err.message}`),
button({ on: { click: retry } }, "Retry"),
]) as HTMLElement,
},
() => MainContent(),
);
}
```

Expand Down
24 changes: 13 additions & 11 deletions docs/examples/dashboard-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,22 +214,24 @@ function App(): HTMLElement {
div({
class: "content",
nodes: [
ErrorBoundary({
nodes: () =>
ErrorBoundary(
{
fallback: (err, retry) =>
div({
class: "error-panel",
nodes: [
p({ nodes: `Error: ${err.message}` }),
button({ nodes: "Retry", on: { click: retry } }),
],
}) as HTMLElement,
},
() =>
Suspense({
fallback: () =>
div({ class: "loading", nodes: "Loading..." }) as HTMLElement,
nodes: () => Outlet(),
}),
fallback: (err, retry) =>
div({
class: "error-panel",
nodes: [
p({ nodes: `Error: ${err.message}` }),
button({ nodes: "Retry", on: { click: retry } }),
],
}) as HTMLElement,
}),
),
],
}),
],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sibujs",
"version": "2.2.0",
"version": "3.0.0",
"description": "A lightweight, function-based frontend framework that combines the best of React, Svelte, and Vue — with zero VDOM and maximum simplicity. Designed for developers who want fine-grained reactivity and full control without compilation or magic.",
"keywords": [
"frontend",
Expand Down
39 changes: 24 additions & 15 deletions src/components/ErrorBoundary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ import { effect } from "../core/signals/effect";
import { signal } from "../core/signals/signal";
import { ErrorDisplay } from "./ErrorDisplay";

export interface ErrorBoundaryProps {
/**
* Function that renders child content or throws.
*/
nodes: () => Element;
export interface ErrorBoundaryOptions {
/**
* Fallback renderer given an Error and retry callback.
* Memoized internally — only re-created when the error changes.
Expand All @@ -30,15 +26,18 @@ export interface ErrorBoundaryProps {
* @example
* ```ts
* const [route, setRoute] = signal("/");
* ErrorBoundary({
* resetKeys: [route],
* nodes: () => div(riskyPageFor(route())),
* });
* ErrorBoundary(
* { resetKeys: [route] },
* () => div(riskyPageFor(route())),
* );
* ```
*/
resetKeys?: Array<() => unknown>;
}

/** @deprecated Renamed to `ErrorBoundaryOptions`; kept for typing compatibility. */
export type ErrorBoundaryProps = ErrorBoundaryOptions;

// CSS styles for ErrorBoundary
const errorBoundaryStyles = `
.sibu-error-boundary {
Expand Down Expand Up @@ -250,15 +249,25 @@ function getMemoizedFallback(
* ErrorBoundary component using SibuJS reactive pattern.
*
* Features:
* - Catches sync errors thrown by nodes
* - Catches async errors (Promise rejections) from nodes
* - Catches sync errors thrown by children
* - Catches async errors (Promise rejections) from children
* - Supports nested ErrorBoundaries (inner catches first, outer catches propagation)
* - Retry functionality to clear error and re-render nodes
* - Retry functionality to clear error and re-render children
* - Memoized fallback to avoid re-creating fallback UI on every render
* - onError callback for logging/telemetry
* - Improved CSS styling
*/
export function ErrorBoundary({ nodes, fallback, onError, resetKeys }: ErrorBoundaryProps): Element {
export function ErrorBoundary(children: () => Element): Element;
export function ErrorBoundary(options: ErrorBoundaryOptions, children: () => Element): Element;
export function ErrorBoundary(
optionsOrChildren: ErrorBoundaryOptions | (() => Element),
maybeChildren?: () => Element,
): Element {
const children: () => Element =
typeof optionsOrChildren === "function" ? optionsOrChildren : (maybeChildren as () => Element);
const options: ErrorBoundaryOptions = typeof optionsOrChildren === "function" ? {} : optionsOrChildren;
const { fallback, onError, resetKeys } = options;

injectStyles();

const [error, setError] = signal<Error | null>(null);
Expand Down Expand Up @@ -360,9 +369,9 @@ export function ErrorBoundary({ nodes, fallback, onError, resetKeys }: ErrorBoun
}

try {
const result = nodes();
const result = children();

// Handle async nodes (Promise-returning components)
// Handle async children (Promise-returning components)
if (result && typeof (result as unknown as Promise<Element>).then === "function") {
const asyncContainer = div({ class: "sibu-error-async" }) as Element;
asyncContainer.appendChild(span({ class: "sibu-lazy-loading", nodes: "Loading..." }));
Expand Down
18 changes: 10 additions & 8 deletions tests/ErrorBoundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ describe("ErrorBoundary", () => {
const [toggle, setToggle] = signal(false);
const parent = document.createElement("div");

const boundary = ErrorBoundary({
nodes: () => {
const boundary = ErrorBoundary(
{
fallback: (err) => {
const fallbackEl = document.createElement("div");
fallbackEl.textContent = `Fallback: ${err.message}`;
return fallbackEl;
},
},
() => {
if (toggle()) throw new Error("Oops");
const el = document.createElement("span");
el.textContent = "OK";
return el;
},
fallback: (err) => {
const fallbackEl = document.createElement("div");
fallbackEl.textContent = `Fallback: ${err.message}`;
return fallbackEl;
},
});
);

parent.appendChild(boundary);

Expand Down
17 changes: 7 additions & 10 deletions tests/errorBoundary-resetKeys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@ describe("ErrorBoundary / resetKeys", () => {
const [route, setRoute] = signal("/a");
let throwIt = true;

const boundary = ErrorBoundary({
resetKeys: [route],
nodes: () => {
if (throwIt) {
throw new Error("first render failed");
}
const d = document.createElement("div");
d.textContent = "ok";
return d;
},
const boundary = ErrorBoundary({ resetKeys: [route] }, () => {
if (throwIt) {
throw new Error("first render failed");
}
const d = document.createElement("div");
d.textContent = "ok";
return d;
});

document.body.appendChild(boundary);
Expand Down
10 changes: 2 additions & 8 deletions tests/errorBoundary.nested.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ describe("ErrorBoundary nested behavior", () => {

// Compose nested boundaries
const tree = () => {
return ErrorBoundary({
nodes: () => ErrorBoundary({ nodes: BadChild, fallback: innerFallback }),
fallback: outerFallback,
});
return ErrorBoundary({ fallback: outerFallback }, () => ErrorBoundary({ fallback: innerFallback }, BadChild));
};

const { container } = mountComponent(tree);
Expand Down Expand Up @@ -61,10 +58,7 @@ describe("ErrorBoundary nested behavior", () => {
};

const tree = () => {
return ErrorBoundary({
nodes: () => ErrorBoundary({ nodes: BadChild, fallback: BadFallback }),
fallback: outerFallback,
});
return ErrorBoundary({ fallback: outerFallback }, () => ErrorBoundary({ fallback: BadFallback }, BadChild));
};

const { container } = mountComponent(tree);
Expand Down
Loading
Loading