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 diff --git a/packages/runed/src/lib/utilities/resource/resource.svelte.ts b/packages/runed/src/lib/utilities/resource/resource.svelte.ts index 418ddd1b..242baf4c 100644 --- a/packages/runed/src/lib/utilities/resource/resource.svelte.ts +++ b/packages/runed/src/lib/utilities/resource/resource.svelte.ts @@ -1,5 +1,6 @@ import { watch } from "$lib/utilities/watch/index.js"; import type { Getter } from "$lib/internal/types.js"; +import { SvelteMap } from "svelte/reactivity"; /** * Configuration options for the resource function @@ -150,7 +151,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 +173,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 +199,7 @@ function runResource< } return undefined; } finally { - loading = false; + loadings.delete(currentFetchId); } }; @@ -232,7 +235,7 @@ function runResource< return current; }, get loading() { - return loading; + return loadings.get(fetchId) ?? false; }, get error() { return error; 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..b8e5d84e 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,30 @@ 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", () => {