From 14afdcf8da1a9e36f38a86517c3ad9b0b0d2cdb9 Mon Sep 17 00:00:00 2001 From: madaxen86 <59310516+madaxen86@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:13:17 +0200 Subject: [PATCH 1/3] fix: createAsync - catch errors of prev to avoid bubbling error up the tree --- src/data/createAsync.ts | 49 ++++++++++--- test/data.spec.tsx | 156 ++++++++++++++++++++++++++++++++++++++++ test/helpers.ts | 4 ++ tsconfig.json | 6 +- 4 files changed, 201 insertions(+), 14 deletions(-) create mode 100644 test/data.spec.tsx diff --git a/src/data/createAsync.ts b/src/data/createAsync.ts index deb6a4999..0e5fc8e59 100644 --- a/src/data/createAsync.ts +++ b/src/data/createAsync.ts @@ -1,7 +1,14 @@ /** * This is mock of the eventual Solid 2.0 primitive. It is not fully featured. */ -import { type Accessor, createResource, sharedConfig, type Setter, untrack } from "solid-js"; +import { + type Accessor, + createResource, + sharedConfig, + type Setter, + untrack, + catchError +} from "solid-js"; import { createStore, reconcile, type ReconcileOptions, unwrap } from "solid-js/store"; import { isServer } from "solid-js/web"; @@ -13,7 +20,7 @@ import { isServer } from "solid-js/web"; export type AccessorWithLatest = { (): T; latest: T; -} +}; export function createAsync( fn: (prev: T) => Promise, @@ -40,19 +47,28 @@ export function createAsync( } ): AccessorWithLatest { let resource: () => T; - let prev = () => !resource || (resource as any).state === "unresolved" ? undefined : (resource as any).latest; + let prev = () => + !resource || (resource as any).state === "unresolved" ? undefined : (resource as any).latest; + [resource] = createResource( - () => subFetch(fn, untrack(prev)), + () => + subFetch( + fn, + catchError( + () => untrack(prev), + () => undefined + ) + ), v => v, options as any ); const resultAccessor: AccessorWithLatest = (() => resource()) as any; - Object.defineProperty(resultAccessor, 'latest', { + Object.defineProperty(resultAccessor, "latest", { get() { return (resource as any).latest; } - }) + }); return resultAccessor; } @@ -85,9 +101,20 @@ export function createAsyncStore( } = {} ): AccessorWithLatest { let resource: () => T; - let prev = () => !resource || (resource as any).state === "unresolved" ? undefined : unwrap((resource as any).latest); + + let prev = () => + !resource || (resource as any).state === "unresolved" + ? undefined + : unwrap((resource as any).latest); [resource] = createResource( - () => subFetch(fn, untrack(prev)), + () => + subFetch( + fn, + catchError( + () => untrack(prev), + () => undefined + ) + ), v => v, { ...options, @@ -96,11 +123,11 @@ export function createAsyncStore( ); const resultAccessor: AccessorWithLatest = (() => resource()) as any; - Object.defineProperty(resultAccessor, 'latest', { + Object.defineProperty(resultAccessor, "latest", { get() { return (resource as any).latest; } - }) + }); return resultAccessor; } @@ -157,6 +184,8 @@ function subFetch(fn: (prev: T | undefined) => Promise, prev: T | undefine try { window.fetch = () => new MockPromise() as any; Promise = MockPromise as any; + console.log({ prev, fn }); + return fn(prev); } finally { window.fetch = ogFetch; diff --git a/test/data.spec.tsx b/test/data.spec.tsx new file mode 100644 index 000000000..9869f4e6b --- /dev/null +++ b/test/data.spec.tsx @@ -0,0 +1,156 @@ +import { + ErrorBoundary, + ParentProps, + Suspense, + catchError, + createRoot, + createSignal +} from "solid-js"; +import { render } from "solid-js/web"; +import { createAsync, createAsyncStore } from "../src/data"; +import { awaitPromise, waitFor } from "./helpers"; + +function Parent(props: ParentProps) { + return }>{props.children}; +} + +async function getText(arg?: string) { + return arg || "fallback"; +} +async function getError(arg?: any): Promise { + throw Error("error"); +} + +describe("createAsync should", () => { + test("return 'fallback'", () => { + createRoot(() => { + const data = createAsync(() => getText()); + setTimeout(() => expect(data()).toBe("fallback"), 1); + }); + }); + test("return 'text'", () => { + createRoot(() => { + const data = createAsync(() => getText("text")); + setTimeout(() => expect(data()).toBe("text"), 1); + }); + }); + test("initial error to be caught ", () => { + createRoot(() => { + const data = createAsync(() => getError()); + setTimeout(() => catchError(data, err => expect(err).toBeInstanceOf(Error)), 1); + }); + }); + test("catch error after arg change - initial valid", () => + createRoot(async dispose => { + async function throwWhenError(arg: string): Promise { + if (arg === "error") throw new Error("error"); + return arg; + } + + const [arg, setArg] = createSignal(""); + function Child() { + const data = createAsync(() => throwWhenError(arg())); + + return ( +
+ ( +
+
+ )} + > + +

{data()}

+

{data.latest}

+
+
+
+ ); + } + await render( + () => ( + + + + ), + document.body + ); + const childErrorElement = () => document.getElementById("childError"); + const parentErrorElement = document.getElementById("parentError"); + expect(childErrorElement()).toBeNull(); + expect(parentErrorElement).toBeNull(); + setArg("error"); + await awaitPromise(); + + // after changing the arg the error should still be caught by the Child's ErrorBoundary + expect(childErrorElement()).not.toBeNull(); + expect(parentErrorElement).toBeNull(); + + //reset ErrorBoundary + document.getElementById("reset")?.click(); + + expect(childErrorElement()).toBeNull(); + await awaitPromise(); + const dataEl = () => document.getElementById("data"); + + expect(dataEl()).not.toBeNull(); + expect(document.getElementById("data")?.innerHTML).toBe("true"); + expect(document.getElementById("latest")?.innerHTML).toBe("true"); + + document.body.innerHTML = ""; + dispose(); + })); + test("catch consecutive error after initial error change to be caught after arg change", () => + createRoot(async cleanup => { + const [arg, setArg] = createSignal("error"); + function Child() { + const data = createAsync(() => getError(arg())); + + return ( +
+ ( +
+
+ )} + > + {data()} +
+
+ ); + } + await render( + () => ( + + + + ), + document.body + ); + + // Child's ErrorBoundary should catch the error + expect(document.getElementById("childError")).not.toBeNull(); + expect(document.getElementById("parentError")).toBeNull(); + setArg("error_2"); + await awaitPromise(); + // after changing the arg the error should still be caught by the Child's ErrorBoundary + expect(document.getElementById("childError")).not.toBeNull(); + expect(document.getElementById("parentError")).toBeNull(); + + document.getElementById("reset")?.click(); + await awaitPromise(); + expect(document.getElementById("childError")).not.toBeNull(); + expect(document.getElementById("parentError")).toBeNull(); + + document.body.innerHTML = ""; + cleanup(); + })); +}); diff --git a/test/helpers.ts b/test/helpers.ts index 8f2fdc401..26f652497 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -23,3 +23,7 @@ export function createAsyncRoot(fn: (resolve: () => void, disposer: () => void) createRoot(disposer => fn(resolve, disposer)); }); } + +export async function awaitPromise() { + return new Promise(resolve => setTimeout(resolve, 100)); +} diff --git a/tsconfig.json b/tsconfig.json index e6bd1866e..faa7c61b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,6 @@ "outDir": "./dist", "module": "esnext" }, - "include": [ - "./src" - ], + "include": ["./src", "./test"], "exclude": ["node_modules/"] -} \ No newline at end of file +} From 7701aa3a6177c0aa0abb570b628f5f0a7bfd906c Mon Sep 17 00:00:00 2001 From: madaxen86 <59310516+madaxen86@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:34:56 +0200 Subject: [PATCH 2/3] removed console log --- src/data/createAsync.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/data/createAsync.ts b/src/data/createAsync.ts index 0e5fc8e59..67f21634f 100644 --- a/src/data/createAsync.ts +++ b/src/data/createAsync.ts @@ -184,8 +184,6 @@ function subFetch(fn: (prev: T | undefined) => Promise, prev: T | undefine try { window.fetch = () => new MockPromise() as any; Promise = MockPromise as any; - console.log({ prev, fn }); - return fn(prev); } finally { window.fetch = ogFetch; From 957304680d64a0272ea20dbf393f2510f1d688c0 Mon Sep 17 00:00:00 2001 From: madaxen86 <59310516+madaxen86@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:41:40 +0200 Subject: [PATCH 3/3] added tsconfig to test removed "test" from root tsconfig --- test/tsconfig.json | 4 ++++ tsconfig.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 test/tsconfig.json diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 000000000..06cefca04 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./../tsconfig.json", + "include": ["."] +} diff --git a/tsconfig.json b/tsconfig.json index faa7c61b0..022a057fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,6 @@ "outDir": "./dist", "module": "esnext" }, - "include": ["./src", "./test"], + "include": ["./src"], "exclude": ["node_modules/"] }