@hookraft/userequest 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ clearAll: () => clearAll,
24
+ useRequest: () => useRequest
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/useRequest.ts
29
+ var import_react = require("react");
30
+
31
+ // src/store.ts
32
+ var cache = /* @__PURE__ */ new Map();
33
+ var inFlight = /* @__PURE__ */ new Map();
34
+ function getCached(key, cacheTime) {
35
+ const entry = cache.get(key);
36
+ if (!entry) return null;
37
+ if (cacheTime === 0) return null;
38
+ const isStale = Date.now() - entry.timestamp > cacheTime;
39
+ if (isStale) {
40
+ cache.delete(key);
41
+ return null;
42
+ }
43
+ return entry.data;
44
+ }
45
+ function setCached(key, data) {
46
+ cache.set(key, { data, timestamp: Date.now() });
47
+ }
48
+ function clearCached(key) {
49
+ cache.delete(key);
50
+ }
51
+ function getInFlight(key) {
52
+ const entry = inFlight.get(key);
53
+ if (!entry) return null;
54
+ entry.subscribers++;
55
+ return entry.promise;
56
+ }
57
+ function setInFlight(key, promise) {
58
+ inFlight.set(key, { promise, subscribers: 1 });
59
+ }
60
+ function clearInFlight(key) {
61
+ inFlight.delete(key);
62
+ }
63
+ function clearAll() {
64
+ cache.clear();
65
+ inFlight.clear();
66
+ }
67
+
68
+ // src/useRequest.ts
69
+ var defaultFetcher = (key) => fetch(key).then((res) => {
70
+ if (!res.ok) throw new Error(`Request failed: ${res.status} ${res.statusText}`);
71
+ return res.json();
72
+ });
73
+ function useRequest(key, options = {}) {
74
+ const {
75
+ fetcher = defaultFetcher,
76
+ cacheTime = 3e4,
77
+ dedupe = true,
78
+ manual = false,
79
+ onSuccess,
80
+ onError,
81
+ onStatusChange
82
+ } = options;
83
+ const [data, setData] = (0, import_react.useState)(() => {
84
+ var _a;
85
+ if (!key) return void 0;
86
+ return (_a = getCached(key, cacheTime)) != null ? _a : void 0;
87
+ });
88
+ const [status, setStatus] = (0, import_react.useState)(() => {
89
+ if (!key) return "idle";
90
+ const cached = getCached(key, cacheTime);
91
+ return cached !== null ? "success" : "idle";
92
+ });
93
+ const [error, setError] = (0, import_react.useState)(void 0);
94
+ const mountedRef = (0, import_react.useRef)(true);
95
+ const onSuccessRef = (0, import_react.useRef)(onSuccess);
96
+ const onErrorRef = (0, import_react.useRef)(onError);
97
+ const onStatusChangeRef = (0, import_react.useRef)(onStatusChange);
98
+ (0, import_react.useEffect)(() => {
99
+ onSuccessRef.current = onSuccess;
100
+ }, [onSuccess]);
101
+ (0, import_react.useEffect)(() => {
102
+ onErrorRef.current = onError;
103
+ }, [onError]);
104
+ (0, import_react.useEffect)(() => {
105
+ onStatusChangeRef.current = onStatusChange;
106
+ }, [onStatusChange]);
107
+ (0, import_react.useEffect)(() => {
108
+ mountedRef.current = true;
109
+ return () => {
110
+ mountedRef.current = false;
111
+ };
112
+ }, []);
113
+ const updateStatus = (0, import_react.useCallback)((next) => {
114
+ var _a;
115
+ if (!mountedRef.current) return;
116
+ setStatus(next);
117
+ (_a = onStatusChangeRef.current) == null ? void 0 : _a.call(onStatusChangeRef, next);
118
+ }, []);
119
+ const execute = (0, import_react.useCallback)(
120
+ async (forceRefresh = false) => {
121
+ var _a, _b, _c, _d;
122
+ if (!key) return;
123
+ if (!forceRefresh) {
124
+ const cached = getCached(key, cacheTime);
125
+ if (cached !== null) {
126
+ if (mountedRef.current) {
127
+ setData(cached);
128
+ updateStatus("success");
129
+ }
130
+ return;
131
+ }
132
+ } else {
133
+ clearCached(key);
134
+ }
135
+ if (dedupe) {
136
+ const existing = getInFlight(key);
137
+ if (existing) {
138
+ updateStatus("loading");
139
+ try {
140
+ const result = await existing;
141
+ if (!mountedRef.current) return;
142
+ setData(result);
143
+ updateStatus("success");
144
+ (_a = onSuccessRef.current) == null ? void 0 : _a.call(onSuccessRef, result);
145
+ } catch (err) {
146
+ if (!mountedRef.current) return;
147
+ setError(err);
148
+ updateStatus("error");
149
+ (_b = onErrorRef.current) == null ? void 0 : _b.call(onErrorRef, err);
150
+ }
151
+ return;
152
+ }
153
+ }
154
+ updateStatus("loading");
155
+ const promise = fetcher(key);
156
+ if (dedupe) {
157
+ setInFlight(key, promise);
158
+ }
159
+ try {
160
+ const result = await promise;
161
+ setCached(key, result);
162
+ clearInFlight(key);
163
+ if (!mountedRef.current) return;
164
+ setData(result);
165
+ setError(void 0);
166
+ updateStatus("success");
167
+ (_c = onSuccessRef.current) == null ? void 0 : _c.call(onSuccessRef, result);
168
+ } catch (err) {
169
+ clearInFlight(key);
170
+ if (!mountedRef.current) return;
171
+ setError(err);
172
+ updateStatus("error");
173
+ (_d = onErrorRef.current) == null ? void 0 : _d.call(onErrorRef, err);
174
+ }
175
+ },
176
+ [key, cacheTime, dedupe, fetcher, updateStatus]
177
+ );
178
+ (0, import_react.useEffect)(() => {
179
+ if (!key || manual) return;
180
+ execute();
181
+ }, [key, manual, execute]);
182
+ const refetch = (0, import_react.useCallback)(async () => {
183
+ await execute(true);
184
+ }, [execute]);
185
+ const clear = (0, import_react.useCallback)(() => {
186
+ if (!key) return;
187
+ clearCached(key);
188
+ setData(void 0);
189
+ setError(void 0);
190
+ updateStatus("idle");
191
+ }, [key, updateStatus]);
192
+ const is = (0, import_react.useCallback)(
193
+ (s) => status === s,
194
+ [status]
195
+ );
196
+ return {
197
+ data,
198
+ status,
199
+ error,
200
+ isLoading: status === "loading",
201
+ isSuccess: status === "success",
202
+ isError: status === "error",
203
+ is,
204
+ refetch,
205
+ clear
206
+ };
207
+ }
208
+ // Annotate the CommonJS export names for ESM import in node:
209
+ 0 && (module.exports = {
210
+ clearAll,
211
+ useRequest
212
+ });
213
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/useRequest.ts","../src/store.ts"],"sourcesContent":["export { useRequest } from \"./useRequest\"\nexport { clearAll } from \"./store\"\nexport type {\n RequestStatus,\n UseRequestOptions,\n UseRequestReturn,\n CacheEntry,\n} from \"./types\"","import { useState, useEffect, useCallback, useRef } from \"react\"\nimport {\n getCached,\n setCached,\n clearCached,\n getInFlight,\n setInFlight,\n clearInFlight,\n} from \"./store\"\nimport type {\n RequestStatus,\n UseRequestOptions,\n UseRequestReturn,\n} from \"./types\"\n\nconst defaultFetcher = (key: string) =>\n fetch(key).then((res) => {\n if (!res.ok) throw new Error(`Request failed: ${res.status} ${res.statusText}`)\n return res.json()\n })\n\n/**\n * useRequest\n *\n * A deduplication-first data fetching hook.\n * Multiple components requesting the same key at the same time\n * will share a single in-flight request — not fire multiple.\n *\n * All caching is in-memory only (JS RAM). Nothing is written\n * to localStorage, sessionStorage, or any browser storage.\n *\n * @example\n * // All three components share ONE network request\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n */\nexport function useRequest<T = unknown>(\n key: string | null,\n options: UseRequestOptions<T> = {}\n): UseRequestReturn<T> {\n const {\n fetcher = defaultFetcher as (key: string) => Promise<T>,\n cacheTime = 30_000,\n dedupe = true,\n manual = false,\n onSuccess,\n onError,\n onStatusChange,\n } = options\n\n const [data, setData] = useState<T | undefined>(() => {\n if (!key) return undefined\n return getCached<T>(key, cacheTime) ?? undefined\n })\n const [status, setStatus] = useState<RequestStatus>(() => {\n if (!key) return \"idle\"\n const cached = getCached<T>(key, cacheTime)\n return cached !== null ? \"success\" : \"idle\"\n })\n const [error, setError] = useState<unknown>(undefined)\n\n const mountedRef = useRef(true)\n const onSuccessRef = useRef(onSuccess)\n const onErrorRef = useRef(onError)\n const onStatusChangeRef = useRef(onStatusChange)\n\n // Keep callback refs fresh without causing re-runs\n useEffect(() => { onSuccessRef.current = onSuccess }, [onSuccess])\n useEffect(() => { onErrorRef.current = onError }, [onError])\n useEffect(() => { onStatusChangeRef.current = onStatusChange }, [onStatusChange])\n\n useEffect(() => {\n mountedRef.current = true\n return () => { mountedRef.current = false }\n }, [])\n\n const updateStatus = useCallback((next: RequestStatus) => {\n if (!mountedRef.current) return\n setStatus(next)\n onStatusChangeRef.current?.(next)\n }, [])\n\n const execute = useCallback(\n async (forceRefresh = false): Promise<void> => {\n if (!key) return\n\n // Check cache first (unless force refreshing)\n if (!forceRefresh) {\n const cached = getCached<T>(key, cacheTime)\n if (cached !== null) {\n if (mountedRef.current) {\n setData(cached)\n updateStatus(\"success\")\n }\n return\n }\n } else {\n clearCached(key)\n }\n\n // Check for an in-flight request for this key\n if (dedupe) {\n const existing = getInFlight<T>(key)\n if (existing) {\n updateStatus(\"loading\")\n try {\n const result = await existing\n if (!mountedRef.current) return\n setData(result)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n return\n }\n }\n\n // No cache, no in-flight — fire a new request\n updateStatus(\"loading\")\n\n const promise = fetcher(key)\n\n if (dedupe) {\n setInFlight(key, promise)\n }\n\n try {\n const result = await promise\n setCached(key, result)\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setData(result)\n setError(undefined)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n },\n [key, cacheTime, dedupe, fetcher, updateStatus]\n )\n\n // Auto-fetch on mount unless manual mode\n useEffect(() => {\n if (!key || manual) return\n execute()\n }, [key, manual, execute])\n\n const refetch = useCallback(async () => {\n await execute(true)\n }, [execute])\n\n const clear = useCallback(() => {\n if (!key) return\n clearCached(key)\n setData(undefined)\n setError(undefined)\n updateStatus(\"idle\")\n }, [key, updateStatus])\n\n const is = useCallback(\n (s: RequestStatus) => status === s,\n [status]\n )\n\n return {\n data,\n status,\n error,\n isLoading: status === \"loading\",\n isSuccess: status === \"success\",\n isError: status === \"error\",\n is,\n refetch,\n clear,\n }\n}","import type { CacheEntry, InFlightEntry } from \"./types\"\n\n/**\n * Global in-memory cache — lives in JS RAM only.\n * Never touches localStorage, sessionStorage, or any browser storage.\n * Cleared automatically when the page refreshes or tab closes.\n */\nconst cache = new Map<string, CacheEntry<unknown>>()\n\n/**\n * In-flight registry — tracks requests currently in progress.\n * If a request for the same key is already in flight,\n * new subscribers attach to the existing Promise instead of firing a new request.\n */\nconst inFlight = new Map<string, InFlightEntry<unknown>>()\n\nexport function getCached<T>(key: string, cacheTime: number): T | null {\n const entry = cache.get(key) as CacheEntry<T> | undefined\n if (!entry) return null\n if (cacheTime === 0) return null\n const isStale = Date.now() - entry.timestamp > cacheTime\n if (isStale) {\n cache.delete(key)\n return null\n }\n return entry.data\n}\n\nexport function setCached<T>(key: string, data: T): void {\n cache.set(key, { data, timestamp: Date.now() })\n}\n\nexport function clearCached(key: string): void {\n cache.delete(key)\n}\n\nexport function getInFlight<T>(key: string): Promise<T> | null {\n const entry = inFlight.get(key) as InFlightEntry<T> | undefined\n if (!entry) return null\n entry.subscribers++\n return entry.promise\n}\n\nexport function setInFlight<T>(key: string, promise: Promise<T>): void {\n inFlight.set(key, { promise: promise as Promise<unknown>, subscribers: 1 })\n}\n\nexport function clearInFlight(key: string): void {\n inFlight.delete(key)\n}\n\nexport function clearAll(): void {\n cache.clear()\n inFlight.clear()\n}"],"mappings":"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","names":[]}
package/dist/index.js ADDED
@@ -0,0 +1,185 @@
1
+ // src/useRequest.ts
2
+ import { useState, useEffect, useCallback, useRef } from "react";
3
+
4
+ // src/store.ts
5
+ var cache = /* @__PURE__ */ new Map();
6
+ var inFlight = /* @__PURE__ */ new Map();
7
+ function getCached(key, cacheTime) {
8
+ const entry = cache.get(key);
9
+ if (!entry) return null;
10
+ if (cacheTime === 0) return null;
11
+ const isStale = Date.now() - entry.timestamp > cacheTime;
12
+ if (isStale) {
13
+ cache.delete(key);
14
+ return null;
15
+ }
16
+ return entry.data;
17
+ }
18
+ function setCached(key, data) {
19
+ cache.set(key, { data, timestamp: Date.now() });
20
+ }
21
+ function clearCached(key) {
22
+ cache.delete(key);
23
+ }
24
+ function getInFlight(key) {
25
+ const entry = inFlight.get(key);
26
+ if (!entry) return null;
27
+ entry.subscribers++;
28
+ return entry.promise;
29
+ }
30
+ function setInFlight(key, promise) {
31
+ inFlight.set(key, { promise, subscribers: 1 });
32
+ }
33
+ function clearInFlight(key) {
34
+ inFlight.delete(key);
35
+ }
36
+ function clearAll() {
37
+ cache.clear();
38
+ inFlight.clear();
39
+ }
40
+
41
+ // src/useRequest.ts
42
+ var defaultFetcher = (key) => fetch(key).then((res) => {
43
+ if (!res.ok) throw new Error(`Request failed: ${res.status} ${res.statusText}`);
44
+ return res.json();
45
+ });
46
+ function useRequest(key, options = {}) {
47
+ const {
48
+ fetcher = defaultFetcher,
49
+ cacheTime = 3e4,
50
+ dedupe = true,
51
+ manual = false,
52
+ onSuccess,
53
+ onError,
54
+ onStatusChange
55
+ } = options;
56
+ const [data, setData] = useState(() => {
57
+ var _a;
58
+ if (!key) return void 0;
59
+ return (_a = getCached(key, cacheTime)) != null ? _a : void 0;
60
+ });
61
+ const [status, setStatus] = useState(() => {
62
+ if (!key) return "idle";
63
+ const cached = getCached(key, cacheTime);
64
+ return cached !== null ? "success" : "idle";
65
+ });
66
+ const [error, setError] = useState(void 0);
67
+ const mountedRef = useRef(true);
68
+ const onSuccessRef = useRef(onSuccess);
69
+ const onErrorRef = useRef(onError);
70
+ const onStatusChangeRef = useRef(onStatusChange);
71
+ useEffect(() => {
72
+ onSuccessRef.current = onSuccess;
73
+ }, [onSuccess]);
74
+ useEffect(() => {
75
+ onErrorRef.current = onError;
76
+ }, [onError]);
77
+ useEffect(() => {
78
+ onStatusChangeRef.current = onStatusChange;
79
+ }, [onStatusChange]);
80
+ useEffect(() => {
81
+ mountedRef.current = true;
82
+ return () => {
83
+ mountedRef.current = false;
84
+ };
85
+ }, []);
86
+ const updateStatus = useCallback((next) => {
87
+ var _a;
88
+ if (!mountedRef.current) return;
89
+ setStatus(next);
90
+ (_a = onStatusChangeRef.current) == null ? void 0 : _a.call(onStatusChangeRef, next);
91
+ }, []);
92
+ const execute = useCallback(
93
+ async (forceRefresh = false) => {
94
+ var _a, _b, _c, _d;
95
+ if (!key) return;
96
+ if (!forceRefresh) {
97
+ const cached = getCached(key, cacheTime);
98
+ if (cached !== null) {
99
+ if (mountedRef.current) {
100
+ setData(cached);
101
+ updateStatus("success");
102
+ }
103
+ return;
104
+ }
105
+ } else {
106
+ clearCached(key);
107
+ }
108
+ if (dedupe) {
109
+ const existing = getInFlight(key);
110
+ if (existing) {
111
+ updateStatus("loading");
112
+ try {
113
+ const result = await existing;
114
+ if (!mountedRef.current) return;
115
+ setData(result);
116
+ updateStatus("success");
117
+ (_a = onSuccessRef.current) == null ? void 0 : _a.call(onSuccessRef, result);
118
+ } catch (err) {
119
+ if (!mountedRef.current) return;
120
+ setError(err);
121
+ updateStatus("error");
122
+ (_b = onErrorRef.current) == null ? void 0 : _b.call(onErrorRef, err);
123
+ }
124
+ return;
125
+ }
126
+ }
127
+ updateStatus("loading");
128
+ const promise = fetcher(key);
129
+ if (dedupe) {
130
+ setInFlight(key, promise);
131
+ }
132
+ try {
133
+ const result = await promise;
134
+ setCached(key, result);
135
+ clearInFlight(key);
136
+ if (!mountedRef.current) return;
137
+ setData(result);
138
+ setError(void 0);
139
+ updateStatus("success");
140
+ (_c = onSuccessRef.current) == null ? void 0 : _c.call(onSuccessRef, result);
141
+ } catch (err) {
142
+ clearInFlight(key);
143
+ if (!mountedRef.current) return;
144
+ setError(err);
145
+ updateStatus("error");
146
+ (_d = onErrorRef.current) == null ? void 0 : _d.call(onErrorRef, err);
147
+ }
148
+ },
149
+ [key, cacheTime, dedupe, fetcher, updateStatus]
150
+ );
151
+ useEffect(() => {
152
+ if (!key || manual) return;
153
+ execute();
154
+ }, [key, manual, execute]);
155
+ const refetch = useCallback(async () => {
156
+ await execute(true);
157
+ }, [execute]);
158
+ const clear = useCallback(() => {
159
+ if (!key) return;
160
+ clearCached(key);
161
+ setData(void 0);
162
+ setError(void 0);
163
+ updateStatus("idle");
164
+ }, [key, updateStatus]);
165
+ const is = useCallback(
166
+ (s) => status === s,
167
+ [status]
168
+ );
169
+ return {
170
+ data,
171
+ status,
172
+ error,
173
+ isLoading: status === "loading",
174
+ isSuccess: status === "success",
175
+ isError: status === "error",
176
+ is,
177
+ refetch,
178
+ clear
179
+ };
180
+ }
181
+ export {
182
+ clearAll,
183
+ useRequest
184
+ };
185
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useRequest.ts","../src/store.ts"],"sourcesContent":["import { useState, useEffect, useCallback, useRef } from \"react\"\nimport {\n getCached,\n setCached,\n clearCached,\n getInFlight,\n setInFlight,\n clearInFlight,\n} from \"./store\"\nimport type {\n RequestStatus,\n UseRequestOptions,\n UseRequestReturn,\n} from \"./types\"\n\nconst defaultFetcher = (key: string) =>\n fetch(key).then((res) => {\n if (!res.ok) throw new Error(`Request failed: ${res.status} ${res.statusText}`)\n return res.json()\n })\n\n/**\n * useRequest\n *\n * A deduplication-first data fetching hook.\n * Multiple components requesting the same key at the same time\n * will share a single in-flight request — not fire multiple.\n *\n * All caching is in-memory only (JS RAM). Nothing is written\n * to localStorage, sessionStorage, or any browser storage.\n *\n * @example\n * // All three components share ONE network request\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n */\nexport function useRequest<T = unknown>(\n key: string | null,\n options: UseRequestOptions<T> = {}\n): UseRequestReturn<T> {\n const {\n fetcher = defaultFetcher as (key: string) => Promise<T>,\n cacheTime = 30_000,\n dedupe = true,\n manual = false,\n onSuccess,\n onError,\n onStatusChange,\n } = options\n\n const [data, setData] = useState<T | undefined>(() => {\n if (!key) return undefined\n return getCached<T>(key, cacheTime) ?? undefined\n })\n const [status, setStatus] = useState<RequestStatus>(() => {\n if (!key) return \"idle\"\n const cached = getCached<T>(key, cacheTime)\n return cached !== null ? \"success\" : \"idle\"\n })\n const [error, setError] = useState<unknown>(undefined)\n\n const mountedRef = useRef(true)\n const onSuccessRef = useRef(onSuccess)\n const onErrorRef = useRef(onError)\n const onStatusChangeRef = useRef(onStatusChange)\n\n // Keep callback refs fresh without causing re-runs\n useEffect(() => { onSuccessRef.current = onSuccess }, [onSuccess])\n useEffect(() => { onErrorRef.current = onError }, [onError])\n useEffect(() => { onStatusChangeRef.current = onStatusChange }, [onStatusChange])\n\n useEffect(() => {\n mountedRef.current = true\n return () => { mountedRef.current = false }\n }, [])\n\n const updateStatus = useCallback((next: RequestStatus) => {\n if (!mountedRef.current) return\n setStatus(next)\n onStatusChangeRef.current?.(next)\n }, [])\n\n const execute = useCallback(\n async (forceRefresh = false): Promise<void> => {\n if (!key) return\n\n // Check cache first (unless force refreshing)\n if (!forceRefresh) {\n const cached = getCached<T>(key, cacheTime)\n if (cached !== null) {\n if (mountedRef.current) {\n setData(cached)\n updateStatus(\"success\")\n }\n return\n }\n } else {\n clearCached(key)\n }\n\n // Check for an in-flight request for this key\n if (dedupe) {\n const existing = getInFlight<T>(key)\n if (existing) {\n updateStatus(\"loading\")\n try {\n const result = await existing\n if (!mountedRef.current) return\n setData(result)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n return\n }\n }\n\n // No cache, no in-flight — fire a new request\n updateStatus(\"loading\")\n\n const promise = fetcher(key)\n\n if (dedupe) {\n setInFlight(key, promise)\n }\n\n try {\n const result = await promise\n setCached(key, result)\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setData(result)\n setError(undefined)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n },\n [key, cacheTime, dedupe, fetcher, updateStatus]\n )\n\n // Auto-fetch on mount unless manual mode\n useEffect(() => {\n if (!key || manual) return\n execute()\n }, [key, manual, execute])\n\n const refetch = useCallback(async () => {\n await execute(true)\n }, [execute])\n\n const clear = useCallback(() => {\n if (!key) return\n clearCached(key)\n setData(undefined)\n setError(undefined)\n updateStatus(\"idle\")\n }, [key, updateStatus])\n\n const is = useCallback(\n (s: RequestStatus) => status === s,\n [status]\n )\n\n return {\n data,\n status,\n error,\n isLoading: status === \"loading\",\n isSuccess: status === \"success\",\n isError: status === \"error\",\n is,\n refetch,\n clear,\n }\n}","import type { CacheEntry, InFlightEntry } from \"./types\"\n\n/**\n * Global in-memory cache — lives in JS RAM only.\n * Never touches localStorage, sessionStorage, or any browser storage.\n * Cleared automatically when the page refreshes or tab closes.\n */\nconst cache = new Map<string, CacheEntry<unknown>>()\n\n/**\n * In-flight registry — tracks requests currently in progress.\n * If a request for the same key is already in flight,\n * new subscribers attach to the existing Promise instead of firing a new request.\n */\nconst inFlight = new Map<string, InFlightEntry<unknown>>()\n\nexport function getCached<T>(key: string, cacheTime: number): T | null {\n const entry = cache.get(key) as CacheEntry<T> | undefined\n if (!entry) return null\n if (cacheTime === 0) return null\n const isStale = Date.now() - entry.timestamp > cacheTime\n if (isStale) {\n cache.delete(key)\n return null\n }\n return entry.data\n}\n\nexport function setCached<T>(key: string, data: T): void {\n cache.set(key, { data, timestamp: Date.now() })\n}\n\nexport function clearCached(key: string): void {\n cache.delete(key)\n}\n\nexport function getInFlight<T>(key: string): Promise<T> | null {\n const entry = inFlight.get(key) as InFlightEntry<T> | undefined\n if (!entry) return null\n entry.subscribers++\n return entry.promise\n}\n\nexport function setInFlight<T>(key: string, promise: Promise<T>): void {\n inFlight.set(key, { promise: promise as Promise<unknown>, subscribers: 1 })\n}\n\nexport function clearInFlight(key: string): void {\n inFlight.delete(key)\n}\n\nexport function clearAll(): void {\n cache.clear()\n inFlight.clear()\n}"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hookraft/userequest",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Deduplication-first data fetching hook for React. One request fires no matter how many components ask for the same data at the same time.",
5
5
  "author": "virus",
6
6
  "license": "MIT",