react-state-custom 1.0.19 → 1.0.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-state-custom",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "The `react-state-custom` library provides a powerful set of tools for managing shared state in React applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.umd.js",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "scripts": {
17
17
  "build": "vite build",
18
- "dev": "vite",
18
+ "dev": "vite --config vite.config.dev.ts",
19
19
  "test": "echo \"No tests specified\" && exit 0"
20
20
  },
21
21
  "keywords": [
@@ -36,6 +36,8 @@
36
36
  "@types/react": "19",
37
37
  "@types/react-dom": "19",
38
38
  "@vitejs/plugin-react": "^5.0.0",
39
+ "react": "^19.2.0",
40
+ "react-dom": "^19.2.0",
39
41
  "typescript": "^5.8.3",
40
42
  "vite": "7",
41
43
  "vite-bundle-analyzer": "^1.1.0",
@@ -0,0 +1,192 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ }
4
+
5
+ .react-state-dev-btn {
6
+ position: fixed;
7
+ bottom: 30px;
8
+ right: 30px;
9
+ transition: opacity 0.3s;
10
+
11
+ &[data-active="true"] {
12
+ opacity: 0;
13
+ pointer-events: none;
14
+ }
15
+ }
16
+
17
+
18
+ .react-state-dev-container {
19
+ --color: light-dark(#333b3c, #efefec);
20
+ --bg-color: light-dark(#f9f9f9, #212121);
21
+ align-items: stretch;
22
+ font-size: 14px;
23
+ color: var(--color);
24
+ background-color: var(--bg-color);
25
+ position: fixed;
26
+ bottom: 0px;
27
+ right: 0px;
28
+ left: 0px;
29
+ opacity: 0;
30
+ pointer-events: none;
31
+ transition: opacity 0.3s, transform 0.3s;
32
+ transform: translateY(100%);
33
+ padding: 0.5em;
34
+
35
+ &[data-active="true"] {
36
+ opacity: 1;
37
+ pointer-events: all;
38
+ transform: translateY(0%);
39
+ }
40
+
41
+ .close-btn {
42
+ position: absolute;
43
+ top: -20px;
44
+ right: 0px;
45
+ }
46
+
47
+ .main-panel {
48
+ display: flex;
49
+ flex-direction: row;
50
+ gap: 1em;
51
+ padding: 1em;
52
+ resize: vertical;
53
+ min-height: 200px;
54
+ max-height: 60vh;
55
+ background-color: #8882;
56
+ overflow: hidden;
57
+ justify-items: stretch;
58
+
59
+ .state-list {
60
+ max-width: 50%;
61
+ min-width: 100px;
62
+ overflow: auto;
63
+ align-self: stretch;
64
+ resize: horizontal;
65
+
66
+ .state-key {
67
+ cursor: pointer;
68
+ padding: 0.2em;
69
+ border-bottom: solid 1px #8884;
70
+ font-family: monospace;
71
+ overflow: hidden;
72
+ text-overflow: ellipsis;
73
+ white-space: nowrap;
74
+
75
+ &[data-active="true"] {
76
+ background-color: light-dark(#0003, #fff3);
77
+ }
78
+ }
79
+
80
+ }
81
+
82
+ .state-view {
83
+ flex: 1;
84
+ overflow: auto;
85
+ border-inline-start: solid 1px #8888;;
86
+ padding-inline-start: 1em;
87
+ }
88
+ }
89
+ }
90
+
91
+ .jv-root {
92
+ font-family: monospace;
93
+ user-select: none;
94
+
95
+ .jv-name {
96
+ opacity: 0.8;
97
+ }
98
+
99
+ .jv-type {
100
+ opacity: 0.5;
101
+ font-size: smaller;
102
+ padding-inline: 0.4em;
103
+ }
104
+
105
+ .jv-cursor {
106
+ cursor: pointer;
107
+ }
108
+
109
+ .jv-field {
110
+ margin-block: 1px;
111
+ }
112
+
113
+ .jv-field .jv-field {
114
+ transition: border-color 1s, background-color 1s;
115
+ border-color: #0000;
116
+ background-color: #0000;
117
+ border-width: 1px;
118
+ border-style: solid;
119
+
120
+ &.jv-updated {
121
+ transition: border-color 0.00s, background-color 0.00s;
122
+ border-color: #f00;
123
+ background-color: #f001;
124
+ }
125
+ }
126
+
127
+ .jv-field-obj>.jv-value {
128
+ padding-inline-start: 1em;
129
+ margin-inline-start: 0.6em;
130
+ border-inline-start: solid 1px #8888;
131
+ }
132
+
133
+ .jv-field-obj>:first-child>:first-child>.jv-name {
134
+ cursor: pointer;
135
+ }
136
+
137
+
138
+ .jv-field-obj {
139
+ --lv: 0;
140
+
141
+ .jv-field-obj {
142
+ --lv: 1;
143
+
144
+ .jv-field-obj {
145
+ --lv: 2;
146
+
147
+ .jv-field-obj {
148
+ --lv: 3;
149
+
150
+ .jv-field-obj {
151
+ --lv: 4;
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ .jv-field-obj>:first-child {
159
+ container-type: scroll-state;
160
+ position: sticky;
161
+ top: calc(var(--lv, 0) * 1.25em);
162
+ z-index: calc(10 - var(--lv, 0));
163
+
164
+ @container scroll-state(stuck: top) {
165
+ >div {
166
+ background-color: var(--bg-color);
167
+ border-bottom: solid 1px #8884;
168
+ font-weight: bold;
169
+ }
170
+
171
+ }
172
+ }
173
+
174
+
175
+
176
+
177
+ .jv-field-string> :is(.jv-type, .jv-value) {
178
+ color: orange;
179
+ }
180
+
181
+ .jv-field-number> :is(.jv-type, .jv-value) {
182
+ color: red;
183
+ }
184
+
185
+ .jv-field-boolean> :is(.jv-type, .jv-value) {
186
+ color: #08f;
187
+ }
188
+
189
+ .jv-field-function> :is(.jv-type, .jv-value) {
190
+ color: #08f;
191
+ }
192
+ }
@@ -0,0 +1,19 @@
1
+ import "./devTool.css"
2
+ import { useState } from "react"
3
+ import { DevToolState } from "./DevToolState";
4
+
5
+
6
+ export const DevToolContainer = ({ toggleButton = "[x]", ...props }) => {
7
+ const [active, setActive] = useState(false);
8
+ return <>
9
+ <button className="react-state-dev-btn" data-active={active} onClick={() => setActive(true)} {...props}>
10
+ {props?.children ?? "Toggle Dev Tool"}
11
+ </button>
12
+ <div className="react-state-dev-container" data-active={active}>
13
+ <button className="close-btn" onClick={() => setActive(false)}>
14
+ [x]
15
+ </button>
16
+ <DevToolState />
17
+ </div>
18
+ </>
19
+ }
@@ -0,0 +1,316 @@
1
+ import { getContext } from "./state-utils/ctx"
2
+ import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"
3
+ import "./devTool.css"
4
+
5
+ const cache = getContext.cache
6
+
7
+ export const DevToolState = ({ }) => {
8
+ const [selectedKey, setKey] = useState("")
9
+ return <div className="main-panel">
10
+ <div className="state-list">
11
+ {[...cache.keys()]
12
+ .map(e => JSON.parse(e)?.[0])
13
+ .filter(e => e != "auto-ctx")
14
+ .map(e => <div
15
+ className="state-key"
16
+ data-active={e == selectedKey}
17
+ onClick={() => setKey(e)}>{e}
18
+ </div>)}
19
+ </div>
20
+ <div className="state-view" >
21
+ <StateView dataKey={selectedKey} key={selectedKey} />
22
+ </div>
23
+ </div>
24
+ }
25
+
26
+ export const StateView: React.FC<{ dataKey: string }> = ({ dataKey }) => {
27
+ const ctx = getContext(dataKey)
28
+ const [currentData, setCurrentData] = useState({ ...ctx?.data })
29
+
30
+ useEffect(() => {
31
+ let checkState = { ...currentData }
32
+ let interval = setInterval(() => {
33
+
34
+ let isDiff = false
35
+
36
+ for (let i in ctx?.data) {
37
+ if (ctx?.data?.[i] != checkState[i]) {
38
+ checkState[i] = ctx?.data?.[i];
39
+ isDiff = true;
40
+ }
41
+ }
42
+ if (isDiff) setCurrentData({ ...checkState })
43
+ }, 200)
44
+
45
+ return () => clearInterval(interval)
46
+ }, [ctx])
47
+
48
+ return <JSONView
49
+ value={currentData}
50
+ name="ROOT"
51
+ expandLevel={1}
52
+ style={{ }}
53
+ />
54
+ }
55
+
56
+ type JSONViewProps = {
57
+ value: any,
58
+ path?: string[],
59
+ name?: string,
60
+ expandRoot: Record<string, boolean>,
61
+ setExpandRoot: Dispatch<SetStateAction<Record<string, boolean>>>,
62
+ expandLevel: number | boolean,
63
+ currentField?: any
64
+ currentType?: any,
65
+ isGrouped?: boolean,
66
+ }
67
+
68
+
69
+ const splitArray = <T,>(array: T[], max = 10) => {
70
+ return Object.fromEntries(
71
+ new Array(Math.ceil((array.length + 1) / max))
72
+ .fill(0)
73
+ .map((_, i, a) => new Array(i == a.length - 1 ? array.length % max : max)
74
+ .fill(0)
75
+ .map((_, j) => i * max + j)
76
+ )
77
+ .filter(e => e.length)
78
+ .map(keys => [`${keys.at(0)}..${keys.at(-1)}`, Object.fromEntries(
79
+ keys.map(k => [k, array[k]])
80
+ )])
81
+ )
82
+ }
83
+
84
+
85
+ const splitObject = (object: any, max = 10) => {
86
+ const keys = Object.keys(object);
87
+ return Object.fromEntries(
88
+ Array(Math.ceil((keys.length + 1) / max))
89
+ .fill(0)
90
+ .map((_, i, a) => new Array(i == a.length - 1 ? keys.length % max : max)
91
+ .fill(0)
92
+ .map((_, j) => i * max + j)
93
+ )
94
+ .filter(e => e.length)
95
+ .map((e) => e.map(i => keys.at(i)))
96
+ .map(sortedKeys => [
97
+ `${sortedKeys.at(0)?.slice(0, 15)}...${sortedKeys.at(-1)?.slice(0, 15)}`,
98
+ Object.fromEntries(sortedKeys.map(key => [key, object[key as any]]))]
99
+ )
100
+ )
101
+ }
102
+
103
+
104
+ const useExpandState = ({ path, expandLevel, expandRoot, setExpandRoot }: JSONViewProps) => {
105
+ const expandKeys = path?.join("%") ?? "";
106
+
107
+ const defaultExpand = typeof expandLevel == "boolean"
108
+ ? expandLevel
109
+ : (typeof expandLevel == 'number' && expandLevel > 0)
110
+
111
+ const isExpand = useMemo(
112
+ () => expandRoot?.[expandKeys] ?? defaultExpand,
113
+ [expandRoot?.[expandKeys], expandKeys]
114
+ )
115
+
116
+ const setExpand = useCallback(
117
+ (value: boolean) => setExpandRoot((r: object) => ({ ...r, [expandKeys]: value })),
118
+ [expandRoot, expandKeys]
119
+ )
120
+
121
+ return { isExpand, setExpand }
122
+
123
+ }
124
+
125
+ const ChangeFlashWrappper: React.FC<React.ComponentProps<'div'> & { value: any, deepCompare?: boolean }> = ({ value, deepCompare = false, ...rest }) => {
126
+
127
+ const ref = useRef<HTMLElement>(undefined)
128
+ const refValue = useRef(value);
129
+
130
+ useEffect(() => {
131
+ if (ref.current) {
132
+ let isDiff = deepCompare && value && refValue.current
133
+ ? (
134
+ Object.keys(value).length != Object.keys(refValue.current).length
135
+ || Object.keys(value).some(key => value[key] != refValue.current[key])
136
+ ) : value != refValue.current
137
+ if (isDiff) {
138
+ refValue.current = value;
139
+ ref.current.classList.add('jv-updated');
140
+ let t = requestAnimationFrame(() => ref.current?.classList.remove('jv-updated'));
141
+ return () => cancelAnimationFrame(t)
142
+ }
143
+ }
144
+
145
+ }, [value, deepCompare, ref])
146
+
147
+ return <div {...rest} ref={ref as any} />
148
+ }
149
+
150
+ const JSONViewObj: React.FC<JSONViewProps> = (props) => {
151
+
152
+ const {
153
+ currentField,
154
+ value, path = [], name, expandRoot, setExpandRoot,
155
+ expandLevel,
156
+ isGrouped,
157
+ } = props
158
+
159
+ const isArray = value instanceof Array
160
+
161
+ const { isExpand, setExpand } = useExpandState(props)
162
+
163
+ const childExpandLevel = typeof expandLevel == "number" ? expandLevel - 1 : expandLevel
164
+
165
+ const shouldGroup = Object.entries(value).length > 10
166
+
167
+ const groupedChilds = useMemo(
168
+ () => shouldGroup
169
+ ? (value instanceof Array) ? splitArray(value, 10) : splitObject(value, 10)
170
+ : value,
171
+ [value, shouldGroup, splitArray]
172
+ )
173
+
174
+ return (isExpand) ? <ChangeFlashWrappper className="jv-field jv-field-obj" value={value} deepCompare={isGrouped}>
175
+ {currentField && <div>
176
+ <div onClick={() => setExpand(false)}>
177
+ <span className="jv-name">{currentField}</span>
178
+ <span>:</span>
179
+ <span>[-]</span>
180
+ <span className="jv-type">{Object.keys(value).length} items </span>
181
+ <span> {isArray ? "[" : "{"} </span>
182
+ </div>
183
+ </div>}
184
+ <div className="jv-value">
185
+ {Object
186
+ .entries(groupedChilds)
187
+ .map(([k, v], index) => <JSONViewCurr
188
+ {...{
189
+ name, expandRoot, setExpandRoot,
190
+ expandLevel: childExpandLevel,
191
+ value: v,
192
+ isGrouped: shouldGroup,
193
+ }}
194
+ key={[...path, shouldGroup ? index : k].join("%")}
195
+ path={[...path, k]}
196
+ />)}
197
+ </div>
198
+ {currentField && <div>
199
+ <span> {isArray ? "]" : "}"} </span>
200
+ </div>}
201
+ </ChangeFlashWrappper> : <ChangeFlashWrappper className="jv-field jv-field-obj" value={value} deepCompare={isGrouped}>
202
+ <div>
203
+ <div onClick={() => setExpand(true)}>
204
+ <span className="jv-name">{currentField}</span>
205
+ {currentField && <span>:</span>}
206
+ {currentField && <span>[+]</span>}
207
+ <span className="jv-type">{Object.keys(value).length} items </span>
208
+ <span> {isArray ? "[" : "{"} </span>
209
+ <span> ... </span>
210
+ <span> {isArray ? "]" : "}"} </span>
211
+ </div>
212
+ </div>
213
+ </ChangeFlashWrappper>
214
+
215
+ }
216
+
217
+ const StringViewObj: React.FC<JSONViewProps> = (props) => {
218
+
219
+ const { currentType, currentField, value, } = props
220
+
221
+ const { isExpand, setExpand } = useExpandState(props)
222
+
223
+ const useExpand = String(value).length > 50
224
+
225
+ const renderString = useExpand && !isExpand
226
+ ? `${String(value).slice(0, 15)}...${String(value).slice(-15, -1)}`
227
+ : String(value)
228
+
229
+ return <ChangeFlashWrappper
230
+ value={props.value}
231
+ className={`jv-field jv-field-${currentType} ${useExpand ? 'jv-cursor' : ''}`}
232
+ onClick={() => setExpand(!isExpand)}>
233
+ <span className="jv-name">{currentField}</span>
234
+ <span>:</span>
235
+ <span className="jv-type">{currentType}, lng={value?.length}</span>
236
+ <span className="jv-value">"{renderString}"</span>
237
+ <span>,</span>
238
+ </ChangeFlashWrappper>
239
+ }
240
+
241
+
242
+ const FunctionViewObj: React.FC<JSONViewProps> = (props) => {
243
+
244
+ const { currentType, currentField, value, } = props
245
+
246
+ const { isExpand, setExpand } = useExpandState(props)
247
+
248
+ const useExpand = String(value).length > 50
249
+
250
+ const renderString = useExpand && !isExpand
251
+ ? `${String(value).slice(0, 15)}...${String(value).slice(-15, -1)}`
252
+ : String(value)
253
+
254
+ return <ChangeFlashWrappper
255
+ value={props.value}
256
+ className={`jv-field jv-field-${currentType} ${useExpand ? 'jv-cursor' : ''}`}
257
+ onClick={() => setExpand(!isExpand)}>
258
+ <span className="jv-name">{currentField}</span>
259
+ <span>:</span>
260
+ <span className="jv-type">{currentType}</span>
261
+ <span className="jv-value">"{renderString}"</span>
262
+ <span>,</span>
263
+ </ChangeFlashWrappper>
264
+ }
265
+
266
+ const DefaultValueView: React.FC<JSONViewProps> = (props) => {
267
+
268
+ const { currentType, currentField, value, } = props
269
+
270
+ return <ChangeFlashWrappper
271
+ value={props.value}
272
+ className={`jv-field jv-field-${currentType}`}>
273
+ <span className="jv-name">{currentField}</span>
274
+ <span>:</span>
275
+ <span className="jv-type">{currentType}</span>
276
+ <span className="jv-value">{String(value)}</span>
277
+ <span>,</span>
278
+ </ChangeFlashWrappper>
279
+ }
280
+
281
+ const JSONViewCurr: React.FC<Omit<JSONViewProps, 'currentField'>> = (props) => {
282
+
283
+ const { value, path = [], name } = props
284
+
285
+ const currentField = path.at(-1) ?? name ?? undefined;
286
+
287
+ const currentType = typeof value
288
+
289
+ switch (currentType) {
290
+ case "object":
291
+ return <JSONViewObj {...props} {...{ currentField, currentType }} />
292
+ case "string":
293
+ return <StringViewObj {...props} {...{ currentField, currentType }} />
294
+ case "function":
295
+ return <FunctionViewObj {...props} {...{ currentField, currentType }} />
296
+ case "number":
297
+ case "boolean":
298
+ case "bigint":
299
+ case "symbol":
300
+ case "undefined":
301
+ default:
302
+ return <DefaultValueView {...props} {...{ currentField, currentType }} />
303
+ }
304
+ }
305
+
306
+ export const JSONView: React.FC<{ value: any, name?: string, style?: any, expandLevel?: number | boolean }> = ({ value, name, style, expandLevel = false }) => {
307
+
308
+ const [expandRoot, setExpandRoot] = useState<Record<string, boolean>>({})
309
+
310
+ return <div className="jv-root" style={style}>
311
+ <JSONViewCurr
312
+ path={[]}
313
+ {...{ name, value, expandRoot, setExpandRoot, expandLevel }}
314
+ />
315
+ </div>
316
+ }
package/src/Test.tsx ADDED
@@ -0,0 +1,90 @@
1
+
2
+ import { createAutoCtx } from './state-utils/createAutoCtx'
3
+ import { createRootCtx } from './state-utils/createRootCtx'
4
+ import { useCallback, useState } from 'react'
5
+ import { useQuickSubscribe } from './state-utils/useQuickSubscribe'
6
+
7
+ const { useCtxState: useDevCtx } = createAutoCtx(
8
+ createRootCtx(
9
+ "devState",
10
+ ({ }) => {
11
+ const [state, setState] = useState(0)
12
+ return {
13
+ state,
14
+ increase: useCallback(() => setState(f => f + 1), [setState]),
15
+ decrease: useCallback(() => setState(f => f - 1), [setState]),
16
+ }
17
+ }
18
+ )
19
+ )
20
+
21
+
22
+
23
+ const { useCtxState: useDevAdvanceCtx } = createAutoCtx(
24
+ createRootCtx(
25
+ "devADVState",
26
+ ({ id }: { id: string }) => {
27
+ const [counter, setCounter] = useState(0)
28
+ const [state, setState] = useState(0)
29
+ const [history, setHistory] = useState([state])
30
+ const [historyMap, setHistoryMap] = useState({})
31
+
32
+ return {
33
+ id,
34
+ state,
35
+ bifIntState: BigInt(state),
36
+ computed: id + state,
37
+ history,
38
+ counter,
39
+ historyMap,
40
+ testSrt: "aiwioj ".repeat(counter * 3),
41
+ increase: useCallback(() => setState(f => {
42
+ setCounter(c => {
43
+ setHistory(h => [...h, f]);
44
+ setHistoryMap(m => ({ ...m, ['state--' + c]: { f, d: Date.now() } }));
45
+ return c + 1
46
+ })
47
+ return f + 1
48
+ }), [setState]),
49
+ decrease: useCallback(() => setState(f => {
50
+ setCounter(c => {
51
+ setHistory(h => [...h, f]);
52
+ setHistoryMap(m => ({ ...m, ['state--' + c]: { f, d: Date.now() } }));
53
+ return c + 1
54
+ })
55
+ return f - 1
56
+ }), [setState])
57
+ }
58
+ }
59
+ )
60
+ )
61
+
62
+
63
+ export const Test = ({ }) => {
64
+ const { state, decrease, increase } = useQuickSubscribe(useDevCtx({}))
65
+ const { state: advState, computed: advComputed, increase: advIncreaser, decrease: advDecrise } = useQuickSubscribe(useDevAdvanceCtx({ id: "name" }))
66
+
67
+ useDevAdvanceCtx({ id: "2123132" })
68
+ useDevAdvanceCtx({ id: "dfgfd" })
69
+ useDevAdvanceCtx({ id: "443" })
70
+ useDevAdvanceCtx({ id: "w3sef" })
71
+ useDevAdvanceCtx({ id: "erere" })
72
+ useDevAdvanceCtx({ id: "sdfdsf" })
73
+ useDevAdvanceCtx({ id: "asdasd" })
74
+ useDevAdvanceCtx({ id: "66666" })
75
+ useDevAdvanceCtx({ id: "dddd" })
76
+ useDevAdvanceCtx({ id: "eeeee" })
77
+ useDevAdvanceCtx({ id: "44444" })
78
+ useDevAdvanceCtx({ id: ";;;;" })
79
+
80
+ return <div>
81
+ <hr />
82
+ <button onClick={increase}>[+]</button>
83
+ <span>{state}</span>
84
+ <button onClick={decrease}>[-]</button>
85
+ <hr />
86
+ <button onClick={advIncreaser}>[+]</button>
87
+ <span>{advState}:{advComputed}:</span>
88
+ <button onClick={advDecrise}>[-]</button>
89
+ </div>
90
+ }
package/src/dev.tsx ADDED
@@ -0,0 +1,14 @@
1
+ import { createRoot } from 'react-dom/client'
2
+ import { DevToolContainer } from './DevTool'
3
+ import { AutoRootCtx, createAutoCtx } from './state-utils/createAutoCtx'
4
+ import { Test } from './Test'
5
+
6
+
7
+
8
+ createRoot(document.getElementById('root')!).render(
9
+ <>
10
+ <DevToolContainer />
11
+ <Test/>
12
+ <AutoRootCtx/>
13
+ </>
14
+ )
package/src/index.ts CHANGED
@@ -18,4 +18,4 @@ export { useArrayHash } from "./state-utils/useArrayHash"
18
18
 
19
19
  export { useQuickSubscribe } from "./state-utils/useQuickSubscribe"
20
20
 
21
- // export { OBJView } from "./components/ObjectView"
21
+ export { DevToolContainer } from "./DevTool"
@@ -12,7 +12,7 @@ export function debounce<T extends (...args: any[]) => any>(
12
12
  timeout = setTimeout(() => {
13
13
  func(...args);
14
14
  }, wait);
15
- } as any;
15
+ } as any;
16
16
 
17
17
  fn.cancel = () => clearTimeout(timeout!);
18
18
 
@@ -22,10 +22,11 @@ export function debounce<T extends (...args: any[]) => any>(
22
22
  // Memoize function
23
23
  export function memoize<T extends (...args: any[]) => any>(
24
24
  func: T
25
- ): (...args: Parameters<T>) => ReturnType<T> {
25
+ ): ((...args: Parameters<T>) => ReturnType<T>) & { cache: Map<string, ReturnType<T>> } {
26
+
26
27
  const cache = new Map<string, ReturnType<T>>();
27
28
 
28
- return function (...args: Parameters<T>): ReturnType<T> {
29
+ const cachedFunc: any = function (...args: Parameters<T>): ReturnType<T> {
29
30
  const key = JSON.stringify(args);
30
31
  if (cache.has(key)) {
31
32
  return cache.get(key) as ReturnType<T>;
@@ -33,6 +34,10 @@ export function memoize<T extends (...args: any[]) => any>(
33
34
  const result = func(...args);
34
35
  cache.set(key, result);
35
36
  return result;
36
- };
37
+ }
38
+
39
+ cachedFunc.cache = cache;
40
+
41
+ return cachedFunc
37
42
  }
38
43