From bb1230f97f8389b7145b0db5d926e08a0c0bcab1 Mon Sep 17 00:00:00 2001 From: falentio Date: Tue, 4 Nov 2025 20:04:08 +0700 Subject: [PATCH 1/4] fix(resource): fix data races in loading state of resource --- .../lib/utilities/resource/resource.svelte.ts | 42 ++++++++++--------- .../resource/resource.test.svelte.ts | 25 +++++++++++ 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/packages/runed/src/lib/utilities/resource/resource.svelte.ts b/packages/runed/src/lib/utilities/resource/resource.svelte.ts index 418ddd1b..0c2bdcbb 100644 --- a/packages/runed/src/lib/utilities/resource/resource.svelte.ts +++ b/packages/runed/src/lib/utilities/resource/resource.svelte.ts @@ -1,5 +1,7 @@ import { watch } from "$lib/utilities/watch/index.js"; import type { Getter } from "$lib/internal/types.js"; +import { SvelteMap } from "svelte/reactivity"; +import { untrack } from "svelte"; /** * Configuration options for the resource function @@ -58,14 +60,14 @@ export type ResourceFetcher = ( /** Current value of the source */ value: Source extends Array ? { - [K in keyof Source]: Source[K]; - } + [K in keyof Source]: Source[K]; + } : Source, /** Previous value of the source */ previousValue: Source extends Array ? { - [K in keyof Source]: Source[K]; - } + [K in keyof Source]: Source[K]; + } : Source | undefined, info: ResourceFetcherRefetchInfo ) => Promise; @@ -126,7 +128,7 @@ function runResource< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter | Array>, @@ -150,7 +152,8 @@ function runResource< // Create state let current = $state> | undefined>(initialValue); - let loading = $state(false); + const loadings = new SvelteMap() + let fetchId = $state(0); let error = $state(undefined); let cleanupFns = $state void>>([]); @@ -171,8 +174,9 @@ function runResource< previousValue: Source | undefined | Array, refetching: RefetchInfo | boolean = false ): Promise> | undefined> => { + const currentFetchId = ++fetchId; try { - loading = true; + loadings.set(currentFetchId, true); error = undefined; runCleanup(); @@ -196,7 +200,7 @@ function runResource< } return undefined; } finally { - loading = false; + loadings.delete(currentFetchId); } }; @@ -232,7 +236,7 @@ function runResource< return current; }, get loading() { - return loading; + return loadings.get(fetchId) ?? false; }, get error() { return error; @@ -307,7 +311,7 @@ export function resource< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter, @@ -325,7 +329,7 @@ export function resource< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter, @@ -341,7 +345,7 @@ export function resource< Sources, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( sources: { [K in keyof Sources]: Getter }, @@ -359,7 +363,7 @@ export function resource< Sources, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( sources: { [K in keyof Sources]: Getter }, @@ -375,7 +379,7 @@ export function resource< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter | Array>, @@ -420,7 +424,7 @@ export function resourcePre< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter, @@ -438,7 +442,7 @@ export function resourcePre< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter, @@ -454,7 +458,7 @@ export function resourcePre< Sources, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( sources: { @@ -474,7 +478,7 @@ export function resourcePre< Sources, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( sources: { @@ -492,7 +496,7 @@ export function resourcePre< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter | Array>, diff --git a/packages/runed/src/lib/utilities/resource/resource.test.svelte.ts b/packages/runed/src/lib/utilities/resource/resource.test.svelte.ts index b3713a4f..cbea0081 100644 --- a/packages/runed/src/lib/utilities/resource/resource.test.svelte.ts +++ b/packages/runed/src/lib/utilities/resource/resource.test.svelte.ts @@ -118,6 +118,31 @@ describe("resource", () => { expect(fetchedValues).toEqual([1, 2]); expect(promiseResource.current).toBe(2); }); + + testWithEffect("no data race in loading state", async () => { + let input = $state(1); + + const dataRaceResource = resource( + () => input, + async (input, _, { signal }): Promise => { + return new Promise((resolve, reject) => { + signal.onabort = () => { + reject(new Error("Aborted " + input)) + }; + sleep(300).then(() => resolve(input)); + }) + } + ) + for (let i = 0; i < 5; i++) { + await sleep(50); + expect(dataRaceResource.loading).toBe(true); + input += 1 + + } + + await sleep(500); + expect(dataRaceResource.loading).toBe(false); + }) }); describe("error handling", () => { From 48bb2ef61ccf00041dae1dd99a83280d8d16e219 Mon Sep 17 00:00:00 2001 From: falentio Date: Tue, 4 Nov 2025 20:06:52 +0700 Subject: [PATCH 2/4] chore: fix formatting; --- .../lib/utilities/resource/resource.svelte.ts | 32 +++++++++---------- .../resource/resource.test.svelte.ts | 11 +++---- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/packages/runed/src/lib/utilities/resource/resource.svelte.ts b/packages/runed/src/lib/utilities/resource/resource.svelte.ts index 0c2bdcbb..80755c83 100644 --- a/packages/runed/src/lib/utilities/resource/resource.svelte.ts +++ b/packages/runed/src/lib/utilities/resource/resource.svelte.ts @@ -60,14 +60,14 @@ export type ResourceFetcher = ( /** Current value of the source */ value: Source extends Array ? { - [K in keyof Source]: Source[K]; - } + [K in keyof Source]: Source[K]; + } : Source, /** Previous value of the source */ previousValue: Source extends Array ? { - [K in keyof Source]: Source[K]; - } + [K in keyof Source]: Source[K]; + } : Source | undefined, info: ResourceFetcherRefetchInfo ) => Promise; @@ -128,7 +128,7 @@ function runResource< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter | Array>, @@ -152,7 +152,7 @@ function runResource< // Create state let current = $state> | undefined>(initialValue); - const loadings = new SvelteMap() + const loadings = new SvelteMap(); let fetchId = $state(0); let error = $state(undefined); let cleanupFns = $state void>>([]); @@ -311,7 +311,7 @@ export function resource< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter, @@ -329,7 +329,7 @@ export function resource< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter, @@ -345,7 +345,7 @@ export function resource< Sources, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( sources: { [K in keyof Sources]: Getter }, @@ -363,7 +363,7 @@ export function resource< Sources, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( sources: { [K in keyof Sources]: Getter }, @@ -379,7 +379,7 @@ export function resource< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter | Array>, @@ -424,7 +424,7 @@ export function resourcePre< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter, @@ -442,7 +442,7 @@ export function resourcePre< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter, @@ -458,7 +458,7 @@ export function resourcePre< Sources, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( sources: { @@ -478,7 +478,7 @@ export function resourcePre< Sources, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( sources: { @@ -496,7 +496,7 @@ export function resourcePre< Source, Awaited>, RefetchInfo - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any > = ResourceFetcher, >( source: Getter | Array>, diff --git a/packages/runed/src/lib/utilities/resource/resource.test.svelte.ts b/packages/runed/src/lib/utilities/resource/resource.test.svelte.ts index cbea0081..b8e5d84e 100644 --- a/packages/runed/src/lib/utilities/resource/resource.test.svelte.ts +++ b/packages/runed/src/lib/utilities/resource/resource.test.svelte.ts @@ -127,22 +127,21 @@ describe("resource", () => { async (input, _, { signal }): Promise => { return new Promise((resolve, reject) => { signal.onabort = () => { - reject(new Error("Aborted " + input)) + reject(new Error("Aborted " + input)); }; sleep(300).then(() => resolve(input)); - }) + }); } - ) + ); for (let i = 0; i < 5; i++) { await sleep(50); expect(dataRaceResource.loading).toBe(true); - input += 1 - + input += 1; } await sleep(500); expect(dataRaceResource.loading).toBe(false); - }) + }); }); describe("error handling", () => { From 1608351565870eff15417f6515516cda416c81b3 Mon Sep 17 00:00:00 2001 From: Hunter Johnston <64506580+huntabyte@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:58:29 -0500 Subject: [PATCH 3/4] add changeset --- .changeset/flat-eggs-reply.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/flat-eggs-reply.md diff --git a/.changeset/flat-eggs-reply.md b/.changeset/flat-eggs-reply.md new file mode 100644 index 00000000..1041628f --- /dev/null +++ b/.changeset/flat-eggs-reply.md @@ -0,0 +1,5 @@ +--- +"runed": patch +--- + +fix(resource): ensure data does not race From af9bc044f0b7c2b18d694a0df385a7f2eead6b98 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Thu, 6 Nov 2025 19:13:45 -0500 Subject: [PATCH 4/4] remove unused --- packages/runed/src/lib/utilities/resource/resource.svelte.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/runed/src/lib/utilities/resource/resource.svelte.ts b/packages/runed/src/lib/utilities/resource/resource.svelte.ts index 80755c83..242baf4c 100644 --- a/packages/runed/src/lib/utilities/resource/resource.svelte.ts +++ b/packages/runed/src/lib/utilities/resource/resource.svelte.ts @@ -1,7 +1,6 @@ import { watch } from "$lib/utilities/watch/index.js"; import type { Getter } from "$lib/internal/types.js"; import { SvelteMap } from "svelte/reactivity"; -import { untrack } from "svelte"; /** * Configuration options for the resource function