@tradejs/app 1.0.4 → 1.0.6
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/README.md +1 -1
- package/bin/tradejs-app.mjs +122 -20
- package/package.json +18 -8
- package/public/auth-bg.jpg +0 -0
- package/public/next.svg +1 -0
- package/public/og-image-source.svg +91 -0
- package/public/og-image.png +0 -0
- package/public/vercel.svg +1 -0
- package/src/app/api/ai/route.ts +26 -5
- package/src/app/api/files/screenshot/[name]/route.ts +53 -17
- package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +7 -1
- package/src/app/api/scanner/[provider]/route.ts +7 -1
- package/src/app/api/scanner/route.ts +7 -1
- package/src/app/api/user/settings/route.ts +216 -0
- package/src/app/components/Dashboard/MainChart/index.tsx +10 -2
- package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +809 -0
- package/src/app/components/Shared/Sidebar/index.tsx +13 -9
- package/src/app/globals.css +11 -0
- package/src/app/layout.tsx +41 -4
- package/src/app/lib/currentUser.ts +27 -0
- package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +22 -19
- package/src/app/routes/signin/page.tsx +11 -2
- package/src/app/store/data.ts +192 -87
|
@@ -4,6 +4,7 @@ import { Box, Flex, IconButton, VStack } from '@chakra-ui/react';
|
|
|
4
4
|
import { useRouter, usePathname } from 'next/navigation';
|
|
5
5
|
import { signOut } from 'next-auth/react';
|
|
6
6
|
import { FiActivity, FiBarChart2, FiLogOut, FiPlay } from 'react-icons/fi';
|
|
7
|
+
import { AccountSettingsDrawer } from './AccountSettingsDrawer';
|
|
7
8
|
|
|
8
9
|
export const Sidebar = () => {
|
|
9
10
|
const router = useRouter();
|
|
@@ -57,15 +58,18 @@ export const Sidebar = () => {
|
|
|
57
58
|
))}
|
|
58
59
|
</VStack>
|
|
59
60
|
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
<VStack gap={2}>
|
|
62
|
+
<AccountSettingsDrawer />
|
|
63
|
+
<IconButton
|
|
64
|
+
aria-label="Sign out"
|
|
65
|
+
size="md"
|
|
66
|
+
colorPalette="teal"
|
|
67
|
+
variant="outline"
|
|
68
|
+
onClick={() => signOut({ callbackUrl: '/routes/signin' })}
|
|
69
|
+
>
|
|
70
|
+
<FiLogOut />
|
|
71
|
+
</IconButton>
|
|
72
|
+
</VStack>
|
|
69
73
|
</Flex>
|
|
70
74
|
</Box>
|
|
71
75
|
);
|
package/src/app/globals.css
CHANGED
package/src/app/layout.tsx
CHANGED
|
@@ -1,15 +1,52 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
|
-
import { Inter } from 'next/font/google';
|
|
3
2
|
import { ClientOnly } from '@chakra-ui/react';
|
|
4
3
|
import { AppShell } from '@shared/AppShell';
|
|
5
4
|
import Provider from './provider';
|
|
6
5
|
import './globals.css';
|
|
7
6
|
|
|
8
|
-
const
|
|
7
|
+
const fallbackMetadataBase = 'http://localhost:3000';
|
|
8
|
+
|
|
9
|
+
const metadataBase = (() => {
|
|
10
|
+
const rawAppUrl = String(process.env.APP_URL || '').trim();
|
|
11
|
+
|
|
12
|
+
if (!rawAppUrl) {
|
|
13
|
+
return new URL(fallbackMetadataBase);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return new URL(rawAppUrl);
|
|
18
|
+
} catch {
|
|
19
|
+
return new URL(fallbackMetadataBase);
|
|
20
|
+
}
|
|
21
|
+
})();
|
|
9
22
|
|
|
10
23
|
export const metadata: Metadata = {
|
|
24
|
+
metadataBase,
|
|
11
25
|
title: 'TradeJS App',
|
|
12
|
-
description:
|
|
26
|
+
description:
|
|
27
|
+
'TradeJS app for dashboards, backtests, charts, derivatives, and runtime data.',
|
|
28
|
+
applicationName: 'TradeJS App',
|
|
29
|
+
openGraph: {
|
|
30
|
+
title: 'TradeJS App',
|
|
31
|
+
description:
|
|
32
|
+
'TradeJS app for dashboards, backtests, charts, derivatives, and runtime data.',
|
|
33
|
+
type: 'website',
|
|
34
|
+
images: [
|
|
35
|
+
{
|
|
36
|
+
url: '/og-image.png',
|
|
37
|
+
width: 1200,
|
|
38
|
+
height: 630,
|
|
39
|
+
alt: 'TradeJS App',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
twitter: {
|
|
44
|
+
card: 'summary_large_image',
|
|
45
|
+
title: 'TradeJS App',
|
|
46
|
+
description:
|
|
47
|
+
'Dashboards, backtests, charts, derivatives, and runtime data in one UI.',
|
|
48
|
+
images: ['/og-image.png'],
|
|
49
|
+
},
|
|
13
50
|
};
|
|
14
51
|
|
|
15
52
|
export default function RootLayout({
|
|
@@ -19,7 +56,7 @@ export default function RootLayout({
|
|
|
19
56
|
}>) {
|
|
20
57
|
return (
|
|
21
58
|
<html lang="en" suppressHydrationWarning>
|
|
22
|
-
<body
|
|
59
|
+
<body>
|
|
23
60
|
<ClientOnly>
|
|
24
61
|
<Provider>
|
|
25
62
|
<AppShell>{children}</AppShell>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { auth } from '@app/auth';
|
|
2
|
+
|
|
3
|
+
type SessionLike = {
|
|
4
|
+
user?: {
|
|
5
|
+
id?: string;
|
|
6
|
+
name?: string | null;
|
|
7
|
+
};
|
|
8
|
+
} | null;
|
|
9
|
+
|
|
10
|
+
const readSessionUserName = (session: SessionLike) => {
|
|
11
|
+
const fromId = session?.user?.id;
|
|
12
|
+
if (typeof fromId === 'string' && fromId.trim()) {
|
|
13
|
+
return fromId.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const fromName = session?.user?.name;
|
|
17
|
+
if (typeof fromName === 'string' && fromName.trim()) {
|
|
18
|
+
return fromName.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const getCurrentUserName = async (): Promise<string | null> => {
|
|
25
|
+
const session = (await auth()) as SessionLike;
|
|
26
|
+
return readSessionUserName(session);
|
|
27
|
+
};
|
|
@@ -17,6 +17,7 @@ const Dashboard = () => {
|
|
|
17
17
|
const hasBacktestStrategy = searchParams.has('backtestStrategy');
|
|
18
18
|
const backtestId = searchParams.get('backtestId');
|
|
19
19
|
const backtestStrategy = searchParams.get('backtestStrategy');
|
|
20
|
+
const isScreenshotMode = searchParams.get('screenshot') === '1';
|
|
20
21
|
|
|
21
22
|
const parseDashboardPath = useCallback(() => {
|
|
22
23
|
const parts = window.location.pathname.split('/').filter(Boolean);
|
|
@@ -89,32 +90,34 @@ const Dashboard = () => {
|
|
|
89
90
|
<Box
|
|
90
91
|
as="main"
|
|
91
92
|
minH="100vh"
|
|
92
|
-
p={4}
|
|
93
|
+
p={isScreenshotMode ? 0 : 4}
|
|
93
94
|
bg="gray.900"
|
|
94
95
|
display="flex"
|
|
95
96
|
flexDirection="column"
|
|
96
97
|
justifyContent="space-between"
|
|
97
98
|
alignItems="flex-start"
|
|
98
99
|
>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
100
|
+
{!isScreenshotMode && (
|
|
101
|
+
<Filters.Root
|
|
102
|
+
filters={filters}
|
|
103
|
+
tickers={tickers}
|
|
104
|
+
backtestFiles={tests}
|
|
105
|
+
onChangeFilters={onChangeFilters}
|
|
106
|
+
>
|
|
107
|
+
<Flex mb={2} gap={4} alignItems="center" flexDirection="row">
|
|
108
|
+
<Filters.SelectProvider />
|
|
109
|
+
<Filters.SelectSymbol />
|
|
110
|
+
<Filters.FavoriteIndicator />
|
|
111
|
+
<Filters.SelectInterval />
|
|
112
|
+
<Filters.SelectIndicator />
|
|
113
|
+
</Flex>
|
|
114
|
+
<Flex mb={4} gap={4} flexDirection="row">
|
|
115
|
+
<Filters.SelectBacktest />
|
|
116
|
+
</Flex>
|
|
117
|
+
</Filters.Root>
|
|
118
|
+
)}
|
|
116
119
|
<Box position="relative" flex="1" w="full">
|
|
117
|
-
<MainChart />
|
|
120
|
+
<MainChart screenshotMode={isScreenshotMode} />
|
|
118
121
|
</Box>
|
|
119
122
|
</Box>
|
|
120
123
|
</ClientOnly>
|
|
@@ -75,8 +75,17 @@ const SigninContent = () => {
|
|
|
75
75
|
<Text fontSize="sm" opacity={0.7} letterSpacing="0.2em">
|
|
76
76
|
SIGN IN
|
|
77
77
|
</Text>
|
|
78
|
-
<Text
|
|
79
|
-
|
|
78
|
+
<Text
|
|
79
|
+
fontSize="2xl"
|
|
80
|
+
fontWeight="700"
|
|
81
|
+
letterSpacing="-0.03em"
|
|
82
|
+
lineHeight="1"
|
|
83
|
+
color="white"
|
|
84
|
+
>
|
|
85
|
+
<Box as="span">Trade</Box>
|
|
86
|
+
<Box as="span" color="#20c5bd">
|
|
87
|
+
JS
|
|
88
|
+
</Box>
|
|
80
89
|
</Text>
|
|
81
90
|
</Stack>
|
|
82
91
|
|
package/src/app/store/data.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { create } from 'zustand';
|
|
3
|
-
import _ from 'lodash';
|
|
4
3
|
import { get, set } from 'idb-keyval';
|
|
5
4
|
import { useSearchParams } from 'next/navigation';
|
|
6
5
|
import { KlineChartData, Interval, Filters, Provider } from '@tradejs/types';
|
|
@@ -17,8 +16,44 @@ interface DataState {
|
|
|
17
16
|
) => void;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
interface DataRequest {
|
|
20
|
+
key: string;
|
|
21
|
+
provider: Provider;
|
|
22
|
+
symbol: string;
|
|
23
|
+
interval: Interval;
|
|
24
|
+
start: number;
|
|
25
|
+
end: number;
|
|
26
|
+
cacheOnly: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const MIN_CACHED_CANDLES = 2;
|
|
30
|
+
|
|
31
|
+
const getKey = (provider: Provider, symbol: string, interval: Interval) =>
|
|
32
|
+
`${provider}_${symbol}_${interval}`;
|
|
33
|
+
|
|
34
|
+
const getProvider = (provider?: Provider): Provider => provider || 'bybit';
|
|
35
|
+
|
|
36
|
+
const toRequest = (filters: Filters, cacheOnly: boolean): DataRequest => {
|
|
37
|
+
const provider = getProvider(filters.provider);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
key: getKey(provider, filters.symbol, filters.interval),
|
|
41
|
+
provider,
|
|
42
|
+
symbol: filters.symbol,
|
|
43
|
+
interval: filters.interval,
|
|
44
|
+
start: filters.start,
|
|
45
|
+
end: filters.end,
|
|
46
|
+
cacheOnly,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getRequestKey = ({ key, start, end, cacheOnly }: DataRequest) =>
|
|
51
|
+
`${key}_${start}_${end}_${cacheOnly ? 'cache' : 'live'}`;
|
|
52
|
+
|
|
53
|
+
const hasContinuityData = (data: KlineChartData) =>
|
|
54
|
+
data.length > MIN_CACHED_CANDLES;
|
|
55
|
+
|
|
56
|
+
const inFlightRequests = new Map<string, Promise<KlineChartData>>();
|
|
22
57
|
|
|
23
58
|
const useDataStore = create<DataState>((set) => ({
|
|
24
59
|
data: new Map<string, KlineChartData | null>(),
|
|
@@ -26,7 +61,7 @@ const useDataStore = create<DataState>((set) => ({
|
|
|
26
61
|
set(({ data }) => {
|
|
27
62
|
const next = new Map(data);
|
|
28
63
|
|
|
29
|
-
next.set(getKey(
|
|
64
|
+
next.set(getKey(provider, symbol, interval), newData);
|
|
30
65
|
|
|
31
66
|
return {
|
|
32
67
|
data: next,
|
|
@@ -34,110 +69,180 @@ const useDataStore = create<DataState>((set) => ({
|
|
|
34
69
|
}),
|
|
35
70
|
}));
|
|
36
71
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const prevKey = useRef(key);
|
|
40
|
-
const retried = useRef(false);
|
|
41
|
-
const [fulfilled, setFulfilled] = useState(false);
|
|
42
|
-
const storedData = useDataStore((s) => s.data.get(key));
|
|
43
|
-
const setData = useDataStore((s) => s.setData);
|
|
72
|
+
const loadCachedData = async (key: string) =>
|
|
73
|
+
((await get(key)) as KlineChartData | null) ?? [];
|
|
44
74
|
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
const clearCachedData = async (key: string) => {
|
|
76
|
+
await set(key, []);
|
|
77
|
+
};
|
|
47
78
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
const getFetchStart = (start: number, data: KlineChartData) =>
|
|
80
|
+
Math.max(
|
|
81
|
+
start,
|
|
82
|
+
hasContinuityData(data) ? data[data.length - 2]?.timestamp || 0 : 0,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const requestKline = async (
|
|
86
|
+
request: Pick<
|
|
87
|
+
DataRequest,
|
|
88
|
+
'provider' | 'symbol' | 'interval' | 'end' | 'cacheOnly'
|
|
89
|
+
> & { start: number },
|
|
90
|
+
) =>
|
|
91
|
+
kline({
|
|
92
|
+
provider: request.provider,
|
|
93
|
+
symbol: request.symbol,
|
|
94
|
+
interval: request.interval,
|
|
95
|
+
start: request.start,
|
|
96
|
+
end: request.end,
|
|
97
|
+
cacheOnly: request.cacheOnly,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const loadCurrentData = async ({
|
|
101
|
+
key,
|
|
102
|
+
symbol,
|
|
103
|
+
interval,
|
|
104
|
+
}: Pick<DataRequest, 'key' | 'symbol' | 'interval'>) => {
|
|
105
|
+
let currentData = [
|
|
106
|
+
...(useDataStore.getState().data.get(key) ?? []),
|
|
107
|
+
] as KlineChartData;
|
|
108
|
+
|
|
109
|
+
if (currentData.length < MIN_CACHED_CANDLES) {
|
|
110
|
+
const cachedData = await loadCachedData(key);
|
|
111
|
+
|
|
112
|
+
if (hasContinuityData(cachedData)) {
|
|
113
|
+
currentData = [...cachedData];
|
|
53
114
|
}
|
|
115
|
+
}
|
|
54
116
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
let currentData = [...(storedData ?? [])];
|
|
117
|
+
if (hasContinuityData(currentData) && isWrongData(interval, currentData)) {
|
|
118
|
+
console.warn('Wrong kline continuity, drop cache', symbol, interval);
|
|
119
|
+
await clearCachedData(key);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
64
122
|
|
|
65
|
-
|
|
66
|
-
|
|
123
|
+
return currentData;
|
|
124
|
+
};
|
|
67
125
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
126
|
+
const mergeFreshData = async (
|
|
127
|
+
dataRequest: DataRequest,
|
|
128
|
+
currentData: KlineChartData,
|
|
129
|
+
) => {
|
|
130
|
+
const incrementalData = await requestKline({
|
|
131
|
+
...dataRequest,
|
|
132
|
+
start: getFetchStart(dataRequest.start, currentData),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const mergedData = mergeData(currentData, incrementalData);
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
dataRequest.cacheOnly ||
|
|
139
|
+
!hasContinuityData(mergedData) ||
|
|
140
|
+
!isWrongData(dataRequest.interval, mergedData)
|
|
141
|
+
) {
|
|
142
|
+
return mergedData;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.warn(
|
|
146
|
+
'Wrong kline continuity after merge, refetch full',
|
|
147
|
+
dataRequest.symbol,
|
|
148
|
+
dataRequest.interval,
|
|
149
|
+
);
|
|
150
|
+
await clearCachedData(dataRequest.key);
|
|
151
|
+
|
|
152
|
+
const fullData = await requestKline({
|
|
153
|
+
...dataRequest,
|
|
154
|
+
start: dataRequest.start,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return mergeData([], fullData);
|
|
158
|
+
};
|
|
72
159
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
160
|
+
const persistData = async (
|
|
161
|
+
{
|
|
162
|
+
provider,
|
|
163
|
+
symbol,
|
|
164
|
+
interval,
|
|
165
|
+
key,
|
|
166
|
+
}: Pick<DataRequest, 'provider' | 'symbol' | 'interval' | 'key'>,
|
|
167
|
+
data: KlineChartData,
|
|
168
|
+
) => {
|
|
169
|
+
useDataStore.getState().setData(provider, symbol, interval, data);
|
|
170
|
+
await set(key, data);
|
|
171
|
+
};
|
|
78
172
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
173
|
+
const fetchAndStoreData = async (dataRequest: DataRequest) => {
|
|
174
|
+
const requestKey = getRequestKey(dataRequest);
|
|
175
|
+
const existingRequest = inFlightRequests.get(requestKey);
|
|
176
|
+
|
|
177
|
+
if (existingRequest) {
|
|
178
|
+
return existingRequest;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const pendingRequest = (async () => {
|
|
182
|
+
const currentData = await loadCurrentData(dataRequest);
|
|
183
|
+
const finalData = await mergeFreshData(dataRequest, currentData);
|
|
184
|
+
await persistData(dataRequest, finalData);
|
|
185
|
+
|
|
186
|
+
return finalData;
|
|
187
|
+
})().finally(() => {
|
|
188
|
+
inFlightRequests.delete(requestKey);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
inFlightRequests.set(requestKey, pendingRequest);
|
|
192
|
+
|
|
193
|
+
return pendingRequest;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const useData = (filters: Filters) => {
|
|
197
|
+
const searchParams = useSearchParams();
|
|
198
|
+
const cacheOnly = Boolean(searchParams.get('cacheOnly')) ?? false;
|
|
199
|
+
const { end, interval, provider, start, symbol } = filters;
|
|
200
|
+
const dataRequest = useMemo(
|
|
201
|
+
() =>
|
|
202
|
+
toRequest(
|
|
203
|
+
{
|
|
204
|
+
end,
|
|
106
205
|
interval,
|
|
107
|
-
);
|
|
108
|
-
retried.current = true;
|
|
109
|
-
set(key, []);
|
|
110
|
-
const refetchData = await kline({
|
|
111
206
|
provider,
|
|
112
|
-
symbol,
|
|
113
|
-
interval,
|
|
114
207
|
start,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
208
|
+
symbol,
|
|
209
|
+
} as Filters,
|
|
210
|
+
cacheOnly,
|
|
211
|
+
),
|
|
212
|
+
[cacheOnly, end, interval, provider, start, symbol],
|
|
213
|
+
);
|
|
214
|
+
const [fulfilledKey, setFulfilledKey] = useState<string | null>(null);
|
|
215
|
+
const storedData = useDataStore((s) => s.data.get(dataRequest.key));
|
|
216
|
+
|
|
217
|
+
const fulfilled = fulfilledKey === dataRequest.key;
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
let cancelled = false;
|
|
221
|
+
|
|
222
|
+
const updateData = async () => {
|
|
223
|
+
if (!dataRequest.symbol) {
|
|
224
|
+
if (!cancelled) {
|
|
225
|
+
setFulfilledKey(dataRequest.key);
|
|
122
226
|
}
|
|
123
|
-
set(key, cleaned);
|
|
124
227
|
return;
|
|
125
228
|
}
|
|
126
229
|
|
|
127
|
-
|
|
230
|
+
await fetchAndStoreData(dataRequest);
|
|
128
231
|
|
|
129
|
-
if (!
|
|
130
|
-
|
|
232
|
+
if (!cancelled) {
|
|
233
|
+
setFulfilledKey(dataRequest.key);
|
|
131
234
|
}
|
|
132
|
-
|
|
133
|
-
set(key, finalData);
|
|
134
235
|
};
|
|
135
236
|
|
|
136
237
|
void updateData();
|
|
137
|
-
|
|
238
|
+
|
|
239
|
+
return () => {
|
|
240
|
+
cancelled = true;
|
|
241
|
+
};
|
|
242
|
+
}, [dataRequest]);
|
|
138
243
|
|
|
139
244
|
return {
|
|
140
|
-
key,
|
|
245
|
+
key: dataRequest.key,
|
|
141
246
|
data: storedData ?? [],
|
|
142
247
|
fulfilled,
|
|
143
248
|
};
|