Skip to content

Commit 9d28eae

Browse files
committed
fixes, added tests
1 parent c56596a commit 9d28eae

File tree

2 files changed

+259
-0
lines changed

2 files changed

+259
-0
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { createLoadingStore } from "../../../src/ts/signals/util/loadingStore";
2+
import { vi, describe, it, expect, beforeEach } from "vitest";
3+
4+
const mockFetcher = vi.fn();
5+
const initialValue = vi.fn(() => ({ data: null }));
6+
7+
describe("createLoadingStore", () => {
8+
beforeEach(() => {
9+
mockFetcher.mockClear();
10+
initialValue.mockClear();
11+
});
12+
13+
it("should initialize with the correct state", () => {
14+
const store = createLoadingStore(mockFetcher, initialValue);
15+
16+
expect(store.state().state).toBe("unresolved");
17+
expect(store.state().loading).toBe(false);
18+
expect(store.state().ready).toBe(false);
19+
expect(store.state().refreshing).toBe(false);
20+
expect(store.state().error).toBeUndefined();
21+
expect(store.store).toEqual({ data: null });
22+
});
23+
24+
it("should transition to loading when load is called", async () => {
25+
const store = createLoadingStore(mockFetcher, initialValue);
26+
store.load();
27+
28+
expect(store.state().state).toBe("pending");
29+
expect(store.state().loading).toBe(true);
30+
});
31+
32+
it("should call the fetcher when load is called", async () => {
33+
const store = createLoadingStore(mockFetcher, initialValue);
34+
mockFetcher.mockResolvedValueOnce({ data: "test" });
35+
store.load();
36+
37+
await store.ready();
38+
39+
expect(mockFetcher).toHaveBeenCalledTimes(1);
40+
expect(store.state().state).toBe("ready");
41+
expect(store.store).toEqual({ data: "test" });
42+
});
43+
44+
it("should handle error when fetcher fails", async () => {
45+
mockFetcher.mockRejectedValueOnce(new Error("Failed to load"));
46+
const store = createLoadingStore(mockFetcher, initialValue);
47+
48+
store.load();
49+
50+
await expect(store.ready()).rejects.toThrow("Failed to load");
51+
52+
expect(store.state().state).toBe("errored");
53+
expect(store.state().error).toEqual(new Error("Failed to load"));
54+
});
55+
56+
it("should transition to refreshing state on refresh", async () => {
57+
const store = createLoadingStore(mockFetcher, initialValue);
58+
mockFetcher.mockResolvedValueOnce({ data: "test" });
59+
store.load();
60+
61+
store.refresh(); // trigger refresh
62+
expect(store.state().state).toBe("refreshing");
63+
expect(store.state().refreshing).toBe(true);
64+
});
65+
66+
it("should trigger load when refresh is called and shouldLoad is false", async () => {
67+
const store = createLoadingStore(mockFetcher, initialValue);
68+
mockFetcher.mockResolvedValueOnce({ data: "test" });
69+
expect(store.state().state).toBe("unresolved");
70+
71+
store.refresh();
72+
expect(store.state().state).toBe("refreshing");
73+
expect(store.state().refreshing).toBe(true);
74+
75+
// Wait for the store to be ready after fetching
76+
await store.ready();
77+
78+
// Ensure the store's state is 'ready' after the refresh
79+
expect(store.state().state).toBe("ready");
80+
expect(store.store).toEqual({ data: "test" });
81+
});
82+
83+
it("should reset the store to its initial value on reset", async () => {
84+
const store = createLoadingStore(mockFetcher, initialValue);
85+
mockFetcher.mockResolvedValueOnce({ data: "test" });
86+
store.load();
87+
88+
await store.ready();
89+
90+
expect(store.store).toEqual({ data: "test" });
91+
92+
store.reset();
93+
expect(store.state().state).toBe("unresolved");
94+
expect(store.state().loading).toBe(false);
95+
expect(store.store).toEqual({ data: null });
96+
});
97+
98+
it("should handle a promise rejection during reset", async () => {
99+
const store = createLoadingStore(mockFetcher, initialValue);
100+
101+
// Mock the fetcher to resolve with data
102+
mockFetcher.mockResolvedValueOnce({ data: "test" });
103+
104+
// Trigger loading the store
105+
store.load();
106+
107+
// Wait for the store to be ready
108+
await store.ready();
109+
110+
// Ensure the store state after loading
111+
expect(store.state().state).toBe("ready");
112+
expect(store.store).toEqual({ data: "test" });
113+
114+
// Now call reset, which should reject the ready promise
115+
const readyPromise = store.ready(); // Grab the current ready promise
116+
117+
store.reset(); // Call reset, which should reject the promise
118+
119+
// Ensure the promise rejects as expected
120+
await expect(readyPromise).rejects.toThrow("Reset");
121+
122+
// Ensure the state is reset
123+
expect(store.state().state).toBe("unresolved");
124+
expect(store.state().loading).toBe(false);
125+
expect(store.store).toEqual({ data: null });
126+
});
127+
});
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { createSignal, createResource, createEffect } from "solid-js";
2+
import { createStore, Store } from "solid-js/store";
3+
import type { Accessor, Resource } from "solid-js";
4+
import { promiseWithResolvers } from "../../utils/misc";
5+
6+
type State = Pick<Resource<unknown>, "loading" | "error" | "state"> & {
7+
ready: boolean;
8+
refreshing: boolean;
9+
};
10+
11+
export type LoadingStore<T> = {
12+
/**
13+
* request store to be loaded
14+
*/
15+
load: () => void;
16+
17+
/**
18+
* request store to be refreshed
19+
*/
20+
refresh: () => void;
21+
22+
/**
23+
* reset the resource + store
24+
*/
25+
reset: () => void;
26+
27+
/**
28+
* store state
29+
*/
30+
state: Accessor<State>;
31+
32+
/**
33+
* the data store
34+
*/
35+
store: Store<T>;
36+
37+
/**
38+
* promise that resolves when the store is ready.
39+
* rejects if shouldLoad is false
40+
*/
41+
ready: () => Promise<void>;
42+
};
43+
44+
export function createLoadingStore<T extends object>(
45+
fetcher: () => Promise<T>,
46+
initialValue: () => T,
47+
): LoadingStore<T> {
48+
const [shouldLoad, setShouldLoad] = createSignal(false);
49+
const [getState, setState] = createSignal<State>({
50+
state: "unresolved",
51+
loading: false,
52+
ready: false,
53+
refreshing: false,
54+
error: undefined,
55+
});
56+
57+
const [resource, { refetch }] = createResource(
58+
() => (shouldLoad() ? true : null),
59+
async () => {
60+
return fetcher();
61+
},
62+
);
63+
64+
const [store, setStore] = createStore<T>(initialValue());
65+
let ready = promiseWithResolvers();
66+
67+
const updateState = (
68+
state: Resource<unknown>["state"],
69+
// oxlint-disable-next-line typescript/no-explicit-any
70+
error?: any,
71+
): void => {
72+
setState({
73+
state,
74+
loading: state === "pending",
75+
ready: state === "ready",
76+
refreshing: state === "refreshing",
77+
// oxlint-disable-next-line typescript/no-explicit-any typescript/no-unsafe-assignment
78+
error: error,
79+
});
80+
};
81+
82+
createEffect(() => {
83+
if (!shouldLoad()) {
84+
updateState("unresolved");
85+
return;
86+
}
87+
updateState("pending");
88+
89+
if (resource.error !== undefined) {
90+
updateState("errored", resource.error);
91+
ready.reject(resource.error);
92+
ready = promiseWithResolvers();
93+
return;
94+
}
95+
96+
const value = resource();
97+
if (value !== undefined) {
98+
setStore(value);
99+
updateState("ready");
100+
ready.resolve();
101+
ready = promiseWithResolvers();
102+
}
103+
});
104+
105+
return {
106+
load: () => {
107+
if (!shouldLoad()) setShouldLoad(true);
108+
},
109+
refresh: () => {
110+
if (!shouldLoad()) {
111+
setShouldLoad(true);
112+
}
113+
ready = promiseWithResolvers();
114+
updateState("refreshing");
115+
void refetch();
116+
},
117+
reset: () => {
118+
setShouldLoad(false);
119+
120+
setStore(initialValue());
121+
122+
// reject any waiters
123+
//if (ready.state === "pending") {
124+
ready.reject(new Error("Reset"));
125+
//}
126+
ready = promiseWithResolvers();
127+
},
128+
state: getState,
129+
store,
130+
ready: async () => ready.promise,
131+
};
132+
}

0 commit comments

Comments
 (0)