elhala-query
Version:
A lightweight, educational implementation of React Query concepts with Suspense support.
408 lines (398 loc) • 10.6 kB
JavaScript
// src/useQuery.tsx
import { useSyncExternalStore, useRef, useCallback as useCallback2 } from "react";
// src/client-provider.tsx
import {
useContext,
createContext,
useCallback,
useEffect
} from "react";
import { jsx } from "react/jsx-runtime";
var elhalaClientContext = createContext(null);
var useElhalaClient = () => {
const client = useContext(elhalaClientContext);
if (!client) {
throw new Error(
"useElhalaClient must be used within an ElhalaClientProvider"
);
}
return client;
};
var ElhalaProvider = ({
client,
children
}) => {
const onFocus = useCallback(() => {
client.queries.forEach((query) => {
query.subscribers.forEach((sub) => sub.notify());
});
}, [client]);
useEffect(() => {
const controller = new AbortController();
window.addEventListener("focus", onFocus, { signal: controller.signal });
window.addEventListener("visibilitychange", onFocus, {
signal: controller.signal
});
return () => controller.abort();
}, [onFocus]);
return /* @__PURE__ */ jsx(elhalaClientContext.Provider, { value: client, children });
};
// src/queryObserver.ts
function createQueryObserver(client, { queryFn, queryKey, cacheTime, staleTime = 0 }) {
const query = client.getQuery({ queryFn, queryKey, cacheTime });
let staleCheckTimer = null;
const setupStaleCheck = () => {
if (staleTime <= 0 || !query.state.lastUpdated) return;
if (staleCheckTimer) {
clearTimeout(staleCheckTimer);
staleCheckTimer = null;
}
const timeUntilStale = staleTime - (Date.now() - query.state.lastUpdated);
if (timeUntilStale > 0) {
staleCheckTimer = setTimeout(() => {
query.fetch();
}, timeUntilStale);
}
};
const observer = {
notify: () => {
setupStaleCheck();
return query.state;
},
getResults: () => query.state,
fetch: () => {
if (!query.state.lastUpdated || Date.now() - query.state.lastUpdated > staleTime) {
query.fetch();
}
},
subscribe: (callback) => {
observer.notify = () => {
setupStaleCheck();
callback();
return query.state;
};
const unsub = query.subscribe(observer);
observer.fetch();
setupStaleCheck();
return () => {
unsub();
if (staleCheckTimer) {
clearTimeout(staleCheckTimer);
staleCheckTimer = null;
}
};
},
fetchOptimistic: () => {
return query.fetch();
}
};
return observer;
}
// src/useQuery.tsx
function useQuery(options) {
const client = useElhalaClient();
const observer = useRef(null);
if (!observer.current) {
observer.current = createQueryObserver(client, options);
}
useSyncExternalStore(
useCallback2((onStoreChange) => {
return observer.current.subscribe(onStoreChange);
}, []),
() => observer.current?.getResults(),
() => observer.current?.getResults()
);
return observer.current.getResults();
}
// src/useMutation.ts
import { useSyncExternalStore as useSyncExternalStore2, useRef as useRef2, useCallback as useCallback3 } from "react";
// src/mutationObserver.ts
function createMutationObserver(client, options) {
const mutation = client.getMutation(options);
const observer = {
notify: () => mutation.state,
getResults: () => mutation.state,
subscribe: (callback) => {
observer.notify = callback;
const unsub = mutation.subscribe(observer);
return unsub;
}
};
return observer;
}
// src/useMutation.ts
function useMutation(options) {
const client = useElhalaClient();
const observer = useRef2(null);
if (!observer.current) {
observer.current = createMutationObserver(client, options);
}
useSyncExternalStore2(
useCallback3((onStoreChange) => {
return observer.current.subscribe(onStoreChange);
}, []),
() => observer.current?.getResults(),
() => observer.current?.getResults()
);
return observer.current.getResults();
}
// src/mutation.ts
function createMutation({
mutationFn,
mutationKey,
onError,
onMutate,
onSettled,
onSuccess
}) {
let mutation = {
subscribers: [],
subscribe: (subscriber) => {
mutation.subscribers.push(subscriber);
return () => {
mutation.subscribers = mutation.subscribers.filter(
(sub) => sub !== subscriber
);
};
},
setState: (updater) => {
mutation.state = updater(mutation.state);
mutation.subscribers.forEach((sub) => sub.notify());
},
options: {
mutationFn,
mutationKey,
onError,
onMutate,
onSettled,
onSuccess,
mutationHash: JSON.stringify(mutationKey)
},
state: {
data: void 0,
error: void 0,
isLoading: false,
isSuccess: false,
isError: false,
status: "idle",
mutate: async (variables) => {
mutation.setState((state) => ({
...state,
isLoading: true,
isSuccess: false,
isError: false,
status: "loading"
}));
mutation.options.onMutate?.();
try {
const data = await mutation.options.mutationFn(variables);
mutation.setState((state) => ({
...state,
data,
isSuccess: true,
isError: false,
status: "success"
}));
mutation.options.onSuccess?.(data);
} catch (error) {
mutation.setState((state) => ({
...state,
isSuccess: false,
isError: true,
status: "error",
error
}));
mutation.options.onError?.(error);
} finally {
mutation.setState((state) => ({
...state,
isLoading: false,
status: "idle"
}));
mutation.options.onSettled?.();
}
}
}
};
return mutation;
}
// src/query.ts
var DEFAULT_CACHE_TIME = 1e3 * 60 * 5;
function createQuery(client, { queryFn, queryKey, cacheTime = DEFAULT_CACHE_TIME }) {
let queryHash = JSON.stringify(queryKey);
let query = {
subscribers: [],
state: {
data: void 0,
error: void 0,
isLoading: true,
isFetching: true,
isError: false,
status: "loading",
lastUpdated: void 0,
refetch: () => {
query.fetch();
}
},
promise: void 0,
options: {
queryFn,
queryHash,
queryKey
},
gcTimeout: void 0,
scheduleGC: () => {
query.gcTimeout = setTimeout(() => {
client.removeQuery(query);
}, cacheTime);
},
unscheduleGC: () => {
clearTimeout(query.gcTimeout);
},
subscribe: (subscriber) => {
query.subscribers.push(subscriber);
query.unscheduleGC();
return () => {
query.subscribers = query.subscribers.filter(
(sub) => sub !== subscriber
);
if (query.subscribers.length === 0) {
query.scheduleGC();
}
};
},
setState: (updater) => {
query.state = updater(query.state);
query.subscribers.forEach((sub) => sub.notify());
},
fetch: () => {
if (!query.promise) {
query.promise = (async () => {
query.setState((state) => ({ ...state, isFetching: true }));
try {
let data = await query.options.queryFn({
queryKey: query.options.queryKey
});
query.setState((state) => ({
...state,
data,
status: "success",
isError: false,
lastUpdated: Date.now()
}));
} catch (error) {
query.setState((state) => ({
...state,
error,
isError: true,
status: "error"
}));
} finally {
query.promise = void 0;
query.setState((state) => ({
...state,
isFetching: false,
isLoading: false
}));
}
})();
}
return query.promise;
}
};
return query;
}
// src/elhalaClient.ts
var ElhalaClient = class {
constructor() {
this.queries = /* @__PURE__ */ new Map();
this.mutations = /* @__PURE__ */ new Map();
}
getQuery(options) {
let queryHash = JSON.stringify(options.queryKey);
if (this.queries.has(queryHash)) {
return this.queries.get(queryHash);
} else {
let query = createQuery(this, options);
this.queries.set(queryHash, query);
return query;
}
}
getQueryData(options) {
return this.getQuery(options).state.data;
}
setQueryData(options, data) {
let query = this.getQuery(options);
query.setState((state) => ({
...state,
data
}));
}
removeQuery(query) {
this.queries.delete(query.options.queryHash);
}
invalidateQueries({ queryKey }) {
this.queries.forEach((query) => {
if (queryKey.length === 0 || queryKey.some((key) => query.options.queryKey.includes(key))) {
query.setState((state) => ({
...state,
isLoading: true,
isFetching: true
}));
query.fetch();
}
});
}
clearQueriesCache() {
this.queries.clear();
}
getMutation(options) {
let mutationHash = JSON.stringify(options.mutationKey);
if (this.mutations.has(mutationHash)) {
return this.mutations.get(mutationHash);
} else {
let mutation = createMutation(options);
this.mutations.set(mutationHash, mutation);
return mutation;
}
}
clearMutationsCache() {
this.mutations.clear();
}
};
// src/useSuspenseQuery.ts
import { useSyncExternalStore as useSyncExternalStore3, useRef as useRef3, useCallback as useCallback4 } from "react";
function useSuspenseQuery(options) {
const client = useElhalaClient();
const observer = useRef3(null);
if (!observer.current) {
observer.current = createQueryObserver(client, options);
}
const state = useSyncExternalStore3(
useCallback4(
(onStoreChange) => observer.current.subscribe(onStoreChange),
[]
),
() => observer.current?.getResults(),
() => observer.current?.getResults()
);
if (state?.isFetching) {
const promise = observer.current.fetchOptimistic();
if (promise) throw promise;
throw new Promise(() => {
});
}
if (state?.status === "error") {
throw state.error;
}
return state;
}
export {
ElhalaClient,
ElhalaProvider,
useElhalaClient,
useMutation,
useQuery,
useSuspenseQuery
};
//# sourceMappingURL=index.mjs.map