@module-federation/bridge-react 0.15.0 → 0.17.0
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/CHANGELOG.md +21 -0
- package/__tests__/bridge.spec.tsx +7 -7
- package/__tests__/createLazyComponent.spec.tsx +210 -0
- package/__tests__/prefetch.spec.ts +153 -0
- package/__tests__/setupTests.ts +3 -0
- package/dist/{bridge-base-P6pEjY1q.js → bridge-base-BoshEggF.mjs} +1 -1
- package/dist/{bridge-base-BBH982Tz.cjs → bridge-base-UGCwcMnG.js} +1 -1
- package/dist/data-fetch-server-middleware.cjs.js +163 -0
- package/dist/data-fetch-server-middleware.d.ts +15 -0
- package/dist/data-fetch-server-middleware.es.js +164 -0
- package/dist/data-fetch-utils.cjs.js +24 -0
- package/dist/data-fetch-utils.d.ts +81 -0
- package/dist/data-fetch-utils.es.js +26 -0
- package/dist/index-C0fDZB5b.js +45 -0
- package/dist/index-CqxytsLY.mjs +46 -0
- package/dist/index.cjs.js +35 -9
- package/dist/index.d.ts +140 -0
- package/dist/index.es.js +38 -12
- package/dist/index.esm-BCeUd-x9.mjs +418 -0
- package/dist/index.esm-j_1sIRzg.js +417 -0
- package/dist/lazy-load-component-plugin-C1tVve-W.js +521 -0
- package/dist/lazy-load-component-plugin-PERjiaFJ.mjs +522 -0
- package/dist/lazy-load-component-plugin.cjs.js +6 -0
- package/dist/lazy-load-component-plugin.d.ts +16 -0
- package/dist/lazy-load-component-plugin.es.js +6 -0
- package/dist/lazy-utils.cjs.js +24 -0
- package/dist/lazy-utils.d.ts +149 -0
- package/dist/lazy-utils.es.js +24 -0
- package/dist/plugin.d.ts +13 -4
- package/dist/prefetch-CZvoIftg.js +1334 -0
- package/dist/prefetch-Dux8GUpr.mjs +1335 -0
- package/dist/router-v5.cjs.js +1 -1
- package/dist/router-v5.d.ts +9 -0
- package/dist/router-v5.es.js +1 -1
- package/dist/router-v6.cjs.js +1 -1
- package/dist/router-v6.d.ts +9 -0
- package/dist/router-v6.es.js +1 -1
- package/dist/router.cjs.js +1 -1
- package/dist/router.d.ts +9 -0
- package/dist/router.es.js +1 -1
- package/dist/utils-Cy-amYU5.mjs +2016 -0
- package/dist/utils-iEVlDmyk.js +2015 -0
- package/dist/v18.cjs.js +1 -1
- package/dist/v18.d.ts +9 -0
- package/dist/v18.es.js +1 -1
- package/dist/v19.cjs.js +1 -1
- package/dist/v19.d.ts +9 -0
- package/dist/v19.es.js +1 -1
- package/package.json +47 -5
- package/src/index.ts +32 -1
- package/src/lazy/AwaitDataFetch.tsx +215 -0
- package/src/lazy/constant.ts +30 -0
- package/src/lazy/createLazyComponent.tsx +411 -0
- package/src/lazy/data-fetch/cache.ts +291 -0
- package/src/lazy/data-fetch/call-data-fetch.ts +13 -0
- package/src/lazy/data-fetch/data-fetch-server-middleware.ts +196 -0
- package/src/lazy/data-fetch/index.ts +16 -0
- package/src/lazy/data-fetch/inject-data-fetch.ts +109 -0
- package/src/lazy/data-fetch/prefetch.ts +106 -0
- package/src/lazy/data-fetch/runtime-plugin.ts +115 -0
- package/src/lazy/index.ts +35 -0
- package/src/lazy/logger.ts +6 -0
- package/src/lazy/types.ts +75 -0
- package/src/lazy/utils.ts +372 -0
- package/src/lazy/wrapNoSSR.tsx +10 -0
- package/src/plugins/lazy-load-component-plugin.spec.ts +21 -0
- package/src/plugins/lazy-load-component-plugin.ts +57 -0
- package/src/provider/plugin.ts +4 -4
- package/src/remote/component.tsx +3 -3
- package/src/remote/create.tsx +17 -4
- package/tsconfig.json +1 -1
- package/vite.config.ts +13 -0
- package/vitest.config.ts +6 -1
- package/dist/index-Cv3p6r66.cjs +0 -235
- package/dist/index-D4yt7Udv.js +0 -236
- package/src/.eslintrc.js +0 -9
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import type { ModuleFederation, getInstance } from '@module-federation/runtime';
|
|
2
|
+
import type { BasicProviderModuleInfo } from '@module-federation/sdk';
|
|
3
|
+
import React, { ReactNode, useState, useEffect } from 'react';
|
|
4
|
+
import type { ErrorInfo } from './AwaitDataFetch';
|
|
5
|
+
import type { DataFetchParams, NoSSRRemoteInfo } from './types';
|
|
6
|
+
|
|
7
|
+
import logger from './logger';
|
|
8
|
+
import {
|
|
9
|
+
AwaitDataFetch,
|
|
10
|
+
DelayedLoading,
|
|
11
|
+
transformError,
|
|
12
|
+
} from './AwaitDataFetch';
|
|
13
|
+
import {
|
|
14
|
+
fetchData,
|
|
15
|
+
getDataFetchItem,
|
|
16
|
+
getDataFetchMapKey,
|
|
17
|
+
getDataFetchInfo,
|
|
18
|
+
getLoadedRemoteInfos,
|
|
19
|
+
setDataFetchItemLoadedStatus,
|
|
20
|
+
wrapDataFetchId,
|
|
21
|
+
} from './utils';
|
|
22
|
+
import {
|
|
23
|
+
DATA_FETCH_ERROR_PREFIX,
|
|
24
|
+
DATA_FETCH_FUNCTION,
|
|
25
|
+
FS_HREF,
|
|
26
|
+
LOAD_REMOTE_ERROR_PREFIX,
|
|
27
|
+
MF_DATA_FETCH_TYPE,
|
|
28
|
+
} from './constant';
|
|
29
|
+
|
|
30
|
+
export type IProps = {
|
|
31
|
+
id: string;
|
|
32
|
+
instance: ReturnType<typeof getInstance>;
|
|
33
|
+
injectScript?: boolean;
|
|
34
|
+
injectLink?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type CreateLazyComponentOptions<T, E extends keyof T> = {
|
|
38
|
+
loader: () => Promise<T>;
|
|
39
|
+
instance: ReturnType<typeof getInstance>;
|
|
40
|
+
loading: React.ReactNode;
|
|
41
|
+
delayLoading?: number;
|
|
42
|
+
fallback: ReactNode | ((errorInfo: ErrorInfo) => ReactNode);
|
|
43
|
+
export?: E;
|
|
44
|
+
dataFetchParams?: DataFetchParams;
|
|
45
|
+
noSSR?: boolean;
|
|
46
|
+
injectScript?: boolean;
|
|
47
|
+
injectLink?: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type ReactKey = { key?: React.Key | null };
|
|
51
|
+
|
|
52
|
+
function getTargetModuleInfo(
|
|
53
|
+
id: string,
|
|
54
|
+
instance?: ModuleFederation,
|
|
55
|
+
):
|
|
56
|
+
| {
|
|
57
|
+
module: BasicProviderModuleInfo['modules'][0];
|
|
58
|
+
publicPath: string;
|
|
59
|
+
remoteEntry: string;
|
|
60
|
+
}
|
|
61
|
+
| undefined {
|
|
62
|
+
if (!instance) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const loadedRemoteInfo = getLoadedRemoteInfos(id, instance);
|
|
66
|
+
if (!loadedRemoteInfo) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const { snapshot } = loadedRemoteInfo;
|
|
70
|
+
if (!snapshot) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const publicPath: string =
|
|
74
|
+
'publicPath' in snapshot
|
|
75
|
+
? snapshot.publicPath
|
|
76
|
+
: 'getPublicPath' in snapshot
|
|
77
|
+
? new Function(snapshot.getPublicPath)()
|
|
78
|
+
: '';
|
|
79
|
+
if (!publicPath) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const modules = 'modules' in snapshot ? snapshot.modules : [];
|
|
83
|
+
const targetModule = modules.find(
|
|
84
|
+
(m) => m.modulePath === loadedRemoteInfo.expose,
|
|
85
|
+
);
|
|
86
|
+
if (!targetModule) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const remoteEntry = 'remoteEntry' in snapshot ? snapshot.remoteEntry : '';
|
|
91
|
+
if (!remoteEntry) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
module: targetModule,
|
|
96
|
+
publicPath,
|
|
97
|
+
remoteEntry,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function collectSSRAssets(options: IProps) {
|
|
102
|
+
const {
|
|
103
|
+
id,
|
|
104
|
+
injectLink = true,
|
|
105
|
+
injectScript = false,
|
|
106
|
+
} = typeof options === 'string' ? { id: options } : options;
|
|
107
|
+
const links: React.ReactNode[] = [];
|
|
108
|
+
const scripts: React.ReactNode[] = [];
|
|
109
|
+
const instance = options.instance;
|
|
110
|
+
if (!instance || (!injectLink && !injectScript)) {
|
|
111
|
+
return [...scripts, ...links];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const moduleAndPublicPath = getTargetModuleInfo(id, instance);
|
|
115
|
+
if (!moduleAndPublicPath) {
|
|
116
|
+
return [...scripts, ...links];
|
|
117
|
+
}
|
|
118
|
+
const { module: targetModule, publicPath, remoteEntry } = moduleAndPublicPath;
|
|
119
|
+
if (injectLink) {
|
|
120
|
+
[...targetModule.assets.css.sync, ...targetModule.assets.css.async]
|
|
121
|
+
.sort()
|
|
122
|
+
.forEach((file, index) => {
|
|
123
|
+
links.push(
|
|
124
|
+
<link
|
|
125
|
+
key={`${file.split('.')[0]}_${index}`}
|
|
126
|
+
href={`${publicPath}${file}`}
|
|
127
|
+
rel="stylesheet"
|
|
128
|
+
type="text/css"
|
|
129
|
+
/>,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (injectScript) {
|
|
135
|
+
scripts.push(
|
|
136
|
+
<script
|
|
137
|
+
async={true}
|
|
138
|
+
key={remoteEntry.split('.')[0]}
|
|
139
|
+
src={`${publicPath}${remoteEntry}`}
|
|
140
|
+
crossOrigin="anonymous"
|
|
141
|
+
/>,
|
|
142
|
+
);
|
|
143
|
+
[...targetModule.assets.js.sync].sort().forEach((file, index) => {
|
|
144
|
+
scripts.push(
|
|
145
|
+
<script
|
|
146
|
+
key={`${file.split('.')[0]}_${index}`}
|
|
147
|
+
async={true}
|
|
148
|
+
src={`${publicPath}${file}`}
|
|
149
|
+
crossOrigin="anonymous"
|
|
150
|
+
/>,
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return [...scripts, ...links];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getServerNeedRemoteInfo(
|
|
159
|
+
loadedRemoteInfo: ReturnType<typeof getLoadedRemoteInfos>,
|
|
160
|
+
id: string,
|
|
161
|
+
noSSR?: boolean,
|
|
162
|
+
): NoSSRRemoteInfo | undefined {
|
|
163
|
+
if (
|
|
164
|
+
noSSR ||
|
|
165
|
+
(typeof window !== 'undefined' && window.location.href !== window[FS_HREF])
|
|
166
|
+
) {
|
|
167
|
+
if (!loadedRemoteInfo?.version) {
|
|
168
|
+
throw new Error(`${loadedRemoteInfo?.name} version is empty`);
|
|
169
|
+
}
|
|
170
|
+
const { snapshot } = loadedRemoteInfo;
|
|
171
|
+
if (!snapshot) {
|
|
172
|
+
throw new Error(`${loadedRemoteInfo?.name} snapshot is empty`);
|
|
173
|
+
}
|
|
174
|
+
const dataFetchItem = getDataFetchItem(id);
|
|
175
|
+
const isFetchServer =
|
|
176
|
+
dataFetchItem?.[0]?.[1] === MF_DATA_FETCH_TYPE.FETCH_SERVER;
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
isFetchServer &&
|
|
180
|
+
(!('ssrPublicPath' in snapshot) || !snapshot.ssrPublicPath)
|
|
181
|
+
) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`ssrPublicPath is required while fetching ${loadedRemoteInfo?.name} data in SSR project!`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
isFetchServer &&
|
|
189
|
+
(!('ssrRemoteEntry' in snapshot) || !snapshot.ssrRemoteEntry)
|
|
190
|
+
) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`ssrRemoteEntry is required while loading ${loadedRemoteInfo?.name} data loader in SSR project!`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
name: loadedRemoteInfo.name,
|
|
198
|
+
version: loadedRemoteInfo.version,
|
|
199
|
+
ssrPublicPath:
|
|
200
|
+
'ssrPublicPath' in snapshot ? snapshot.ssrPublicPath || '' : '',
|
|
201
|
+
ssrRemoteEntry:
|
|
202
|
+
'ssrRemoteEntry' in snapshot ? snapshot.ssrRemoteEntry || '' : '',
|
|
203
|
+
globalName: loadedRemoteInfo.entryGlobalName,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function createLazyComponent<T, E extends keyof T>(
|
|
210
|
+
options: CreateLazyComponentOptions<T, E>,
|
|
211
|
+
) {
|
|
212
|
+
const { instance, injectScript, injectLink } = options;
|
|
213
|
+
if (!instance) {
|
|
214
|
+
throw new Error('instance is required for createLazyComponent!');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
type ComponentType = T[E] extends (...args: any) => any
|
|
218
|
+
? Parameters<T[E]>[0] extends undefined
|
|
219
|
+
? ReactKey
|
|
220
|
+
: Parameters<T[E]>[0] & ReactKey
|
|
221
|
+
: ReactKey;
|
|
222
|
+
const exportName = options?.export || 'default';
|
|
223
|
+
|
|
224
|
+
const callLoader = async () => {
|
|
225
|
+
logger.debug('callLoader start', Date.now());
|
|
226
|
+
const m = (await options.loader()) as Record<string, React.FC> &
|
|
227
|
+
Record<symbol, string>;
|
|
228
|
+
logger.debug('callLoader end', Date.now());
|
|
229
|
+
if (!m) {
|
|
230
|
+
throw new Error('load remote failed');
|
|
231
|
+
}
|
|
232
|
+
return m;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const getData = async (noSSR?: boolean) => {
|
|
236
|
+
let loadedRemoteInfo: ReturnType<typeof getLoadedRemoteInfos>;
|
|
237
|
+
let moduleId: string;
|
|
238
|
+
try {
|
|
239
|
+
const m = await callLoader();
|
|
240
|
+
moduleId = m && m[Symbol.for('mf_module_id')];
|
|
241
|
+
if (!moduleId) {
|
|
242
|
+
throw new Error('moduleId is empty');
|
|
243
|
+
}
|
|
244
|
+
loadedRemoteInfo = getLoadedRemoteInfos(moduleId, instance);
|
|
245
|
+
if (!loadedRemoteInfo) {
|
|
246
|
+
throw new Error(`can not find loaded remote('${moduleId}') info!`);
|
|
247
|
+
}
|
|
248
|
+
} catch (e) {
|
|
249
|
+
const errMsg = `${LOAD_REMOTE_ERROR_PREFIX}${e}`;
|
|
250
|
+
logger.debug(e);
|
|
251
|
+
throw new Error(errMsg);
|
|
252
|
+
}
|
|
253
|
+
let dataFetchMapKey: string | undefined;
|
|
254
|
+
try {
|
|
255
|
+
dataFetchMapKey = getDataFetchMapKey(
|
|
256
|
+
getDataFetchInfo({
|
|
257
|
+
name: loadedRemoteInfo.name,
|
|
258
|
+
alias: loadedRemoteInfo.alias,
|
|
259
|
+
id: moduleId,
|
|
260
|
+
remoteSnapshot: loadedRemoteInfo.snapshot,
|
|
261
|
+
}),
|
|
262
|
+
{ name: instance.name, version: instance.options.version },
|
|
263
|
+
);
|
|
264
|
+
logger.debug('getData dataFetchMapKey: ', dataFetchMapKey);
|
|
265
|
+
if (!dataFetchMapKey) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const data = await fetchData(
|
|
269
|
+
dataFetchMapKey,
|
|
270
|
+
{
|
|
271
|
+
...options.dataFetchParams,
|
|
272
|
+
isDowngrade: false,
|
|
273
|
+
},
|
|
274
|
+
getServerNeedRemoteInfo(loadedRemoteInfo, dataFetchMapKey, noSSR),
|
|
275
|
+
);
|
|
276
|
+
setDataFetchItemLoadedStatus(dataFetchMapKey);
|
|
277
|
+
logger.debug('get data res: \n', data);
|
|
278
|
+
return data;
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const errMsg = `${DATA_FETCH_ERROR_PREFIX}${wrapDataFetchId(dataFetchMapKey)}${err}`;
|
|
281
|
+
logger.debug(errMsg);
|
|
282
|
+
throw new Error(errMsg);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const LazyComponent = React.lazy(async () => {
|
|
287
|
+
const m = await callLoader();
|
|
288
|
+
const moduleId = m && m[Symbol.for('mf_module_id')];
|
|
289
|
+
const loadedRemoteInfo = getLoadedRemoteInfos(moduleId, instance);
|
|
290
|
+
loadedRemoteInfo?.snapshot;
|
|
291
|
+
const dataFetchMapKey = loadedRemoteInfo
|
|
292
|
+
? getDataFetchMapKey(
|
|
293
|
+
getDataFetchInfo({
|
|
294
|
+
name: loadedRemoteInfo.name,
|
|
295
|
+
alias: loadedRemoteInfo.alias,
|
|
296
|
+
id: moduleId,
|
|
297
|
+
remoteSnapshot: loadedRemoteInfo.snapshot,
|
|
298
|
+
}),
|
|
299
|
+
{ name: instance.name, version: instance?.options.version },
|
|
300
|
+
)
|
|
301
|
+
: undefined;
|
|
302
|
+
logger.debug('LazyComponent dataFetchMapKey: ', dataFetchMapKey);
|
|
303
|
+
|
|
304
|
+
const assets = collectSSRAssets({
|
|
305
|
+
id: moduleId,
|
|
306
|
+
instance,
|
|
307
|
+
injectLink,
|
|
308
|
+
injectScript,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const Com = m[exportName] as React.FC<ComponentType>;
|
|
312
|
+
if (exportName in m && typeof Com === 'function') {
|
|
313
|
+
return {
|
|
314
|
+
default: (props: Omit<ComponentType, 'key'> & { mfData?: unknown }) => (
|
|
315
|
+
<>
|
|
316
|
+
{globalThis.FEDERATION_SSR && dataFetchMapKey && (
|
|
317
|
+
<script
|
|
318
|
+
suppressHydrationWarning
|
|
319
|
+
dangerouslySetInnerHTML={{
|
|
320
|
+
__html: String.raw`
|
|
321
|
+
globalThis['${DATA_FETCH_FUNCTION}'] = globalThis['${DATA_FETCH_FUNCTION}'] || [];
|
|
322
|
+
globalThis['${DATA_FETCH_FUNCTION}'].push(['${dataFetchMapKey}',${JSON.stringify(props.mfData)}]);
|
|
323
|
+
`,
|
|
324
|
+
}}
|
|
325
|
+
></script>
|
|
326
|
+
)}
|
|
327
|
+
{globalThis.FEDERATION_SSR && assets}
|
|
328
|
+
<Com {...props} />
|
|
329
|
+
</>
|
|
330
|
+
),
|
|
331
|
+
};
|
|
332
|
+
// eslint-disable-next-line max-lines
|
|
333
|
+
} else {
|
|
334
|
+
throw Error(
|
|
335
|
+
`Make sure that ${moduleId} has the correct export when export is ${String(
|
|
336
|
+
exportName,
|
|
337
|
+
)}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return (props: ComponentType) => {
|
|
343
|
+
const { key, ...args } = props;
|
|
344
|
+
|
|
345
|
+
if (!options.noSSR) {
|
|
346
|
+
return (
|
|
347
|
+
<AwaitDataFetch
|
|
348
|
+
resolve={getData(options.noSSR)}
|
|
349
|
+
loading={options.loading}
|
|
350
|
+
delayLoading={options.delayLoading}
|
|
351
|
+
errorElement={options.fallback}
|
|
352
|
+
>
|
|
353
|
+
{/* @ts-expect-error ignore */}
|
|
354
|
+
{(data) => <LazyComponent {...args} mfData={data} />}
|
|
355
|
+
</AwaitDataFetch>
|
|
356
|
+
);
|
|
357
|
+
} else {
|
|
358
|
+
// Client-side rendering logic
|
|
359
|
+
const [data, setData] = useState<unknown>(null);
|
|
360
|
+
const [loading, setLoading] = useState<boolean>(true);
|
|
361
|
+
const [error, setError] = useState<ErrorInfo | null>(null);
|
|
362
|
+
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
let isMounted = true;
|
|
365
|
+
const fetchDataAsync = async () => {
|
|
366
|
+
try {
|
|
367
|
+
setLoading(true);
|
|
368
|
+
const result = await getData(options.noSSR);
|
|
369
|
+
if (isMounted) {
|
|
370
|
+
setData(result);
|
|
371
|
+
}
|
|
372
|
+
} catch (e) {
|
|
373
|
+
if (isMounted) {
|
|
374
|
+
setError(transformError(e as Error));
|
|
375
|
+
}
|
|
376
|
+
} finally {
|
|
377
|
+
if (isMounted) {
|
|
378
|
+
setLoading(false);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
fetchDataAsync();
|
|
384
|
+
|
|
385
|
+
return () => {
|
|
386
|
+
isMounted = false;
|
|
387
|
+
};
|
|
388
|
+
}, []);
|
|
389
|
+
|
|
390
|
+
if (loading) {
|
|
391
|
+
return (
|
|
392
|
+
<DelayedLoading delayLoading={options.delayLoading}>
|
|
393
|
+
{options.loading}
|
|
394
|
+
</DelayedLoading>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (error) {
|
|
399
|
+
return (
|
|
400
|
+
<>
|
|
401
|
+
{typeof options.fallback === 'function'
|
|
402
|
+
? options.fallback(error)
|
|
403
|
+
: options.fallback}
|
|
404
|
+
</>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
// @ts-expect-error ignore
|
|
408
|
+
return <LazyComponent {...args} mfData={data} />;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
import { getDataFetchCache } from '../utils';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
CacheConfig,
|
|
6
|
+
CacheItem,
|
|
7
|
+
DataFetch,
|
|
8
|
+
DataFetchParams,
|
|
9
|
+
} from '../types';
|
|
10
|
+
|
|
11
|
+
export const CacheSize = {
|
|
12
|
+
KB: 1024,
|
|
13
|
+
MB: 1024 * 1024,
|
|
14
|
+
GB: 1024 * 1024 * 1024,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const CacheTime = {
|
|
18
|
+
SECOND: 1000,
|
|
19
|
+
MINUTE: 60 * 1000,
|
|
20
|
+
HOUR: 60 * 60 * 1000,
|
|
21
|
+
DAY: 24 * 60 * 60 * 1000,
|
|
22
|
+
WEEK: 7 * 24 * 60 * 60 * 1000,
|
|
23
|
+
MONTH: 30 * 24 * 60 * 60 * 1000,
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export type CacheStatus = 'hit' | 'stale' | 'miss';
|
|
27
|
+
|
|
28
|
+
export interface CacheStatsInfo {
|
|
29
|
+
status: CacheStatus;
|
|
30
|
+
key: string | symbol;
|
|
31
|
+
params: DataFetchParams;
|
|
32
|
+
result: any;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface CacheOptions {
|
|
36
|
+
tag?: string | string[];
|
|
37
|
+
maxAge?: number;
|
|
38
|
+
revalidate?: number;
|
|
39
|
+
getKey?: <Args extends any[]>(...args: Args) => string;
|
|
40
|
+
onCache?: (info: CacheStatsInfo) => boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getTagKeyMap() {
|
|
44
|
+
const dataFetchCache = getDataFetchCache();
|
|
45
|
+
if (!dataFetchCache || !dataFetchCache.tagKeyMap) {
|
|
46
|
+
const tagKeyMap = new Map<string, Set<string>>();
|
|
47
|
+
globalThis.__MF_DATA_FETCH_CACHE__ ||= {};
|
|
48
|
+
globalThis.__MF_DATA_FETCH_CACHE__.tagKeyMap = tagKeyMap;
|
|
49
|
+
return tagKeyMap;
|
|
50
|
+
}
|
|
51
|
+
return dataFetchCache.tagKeyMap;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function addTagKeyRelation(tag: string, key: string) {
|
|
55
|
+
const tagKeyMap = getTagKeyMap();
|
|
56
|
+
let keys = tagKeyMap.get(tag);
|
|
57
|
+
if (!keys) {
|
|
58
|
+
keys = new Set();
|
|
59
|
+
tagKeyMap.set(tag, keys);
|
|
60
|
+
}
|
|
61
|
+
keys.add(key);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getCacheConfig() {
|
|
65
|
+
const dataFetchCache = getDataFetchCache();
|
|
66
|
+
if (!dataFetchCache || !dataFetchCache.cacheConfig) {
|
|
67
|
+
const cacheConfig: CacheConfig = {
|
|
68
|
+
maxSize: CacheSize.GB,
|
|
69
|
+
};
|
|
70
|
+
globalThis.__MF_DATA_FETCH_CACHE__ ||= {};
|
|
71
|
+
globalThis.__MF_DATA_FETCH_CACHE__.cacheConfig = cacheConfig;
|
|
72
|
+
return cacheConfig;
|
|
73
|
+
}
|
|
74
|
+
return dataFetchCache.cacheConfig;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function configureCache(config: CacheConfig): void {
|
|
78
|
+
const cacheConfig = getCacheConfig();
|
|
79
|
+
Object.assign(cacheConfig, config);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getLRUCache() {
|
|
83
|
+
const dataFetchCache = getDataFetchCache();
|
|
84
|
+
const cacheConfig = getCacheConfig();
|
|
85
|
+
|
|
86
|
+
if (!dataFetchCache || !dataFetchCache.cacheStore) {
|
|
87
|
+
const cacheStore = new LRUCache<string, Map<string, CacheItem<any>>>({
|
|
88
|
+
maxSize: cacheConfig.maxSize ?? CacheSize.GB,
|
|
89
|
+
sizeCalculation: (value: Map<string, CacheItem<any>>): number => {
|
|
90
|
+
if (!value.size) {
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let size = 0;
|
|
95
|
+
for (const [k, item] of value.entries()) {
|
|
96
|
+
size += k.length * 2;
|
|
97
|
+
size += estimateObjectSize(item.data);
|
|
98
|
+
size += 8;
|
|
99
|
+
}
|
|
100
|
+
return size;
|
|
101
|
+
},
|
|
102
|
+
updateAgeOnGet: true,
|
|
103
|
+
updateAgeOnHas: true,
|
|
104
|
+
});
|
|
105
|
+
globalThis.__MF_DATA_FETCH_CACHE__ ||= {};
|
|
106
|
+
globalThis.__MF_DATA_FETCH_CACHE__.cacheStore = cacheStore;
|
|
107
|
+
return cacheStore;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return dataFetchCache.cacheStore;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function estimateObjectSize(data: unknown): number {
|
|
114
|
+
const type = typeof data;
|
|
115
|
+
|
|
116
|
+
if (type === 'number') return 8;
|
|
117
|
+
if (type === 'boolean') return 4;
|
|
118
|
+
if (type === 'string') return Math.max((data as string).length * 2, 1);
|
|
119
|
+
if (data === null || data === undefined) return 1;
|
|
120
|
+
|
|
121
|
+
if (ArrayBuffer.isView(data)) {
|
|
122
|
+
return Math.max(data.byteLength, 1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (Array.isArray(data)) {
|
|
126
|
+
return Math.max(
|
|
127
|
+
data.reduce((acc, item) => acc + estimateObjectSize(item), 0),
|
|
128
|
+
1,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (data instanceof Map || data instanceof Set) {
|
|
133
|
+
return 1024;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (data instanceof Date) {
|
|
137
|
+
return 8;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (type === 'object') {
|
|
141
|
+
return Math.max(
|
|
142
|
+
Object.entries(data).reduce(
|
|
143
|
+
(acc, [key, value]) => acc + key.length * 2 + estimateObjectSize(value),
|
|
144
|
+
0,
|
|
145
|
+
),
|
|
146
|
+
1,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function generateKey(dataFetchOptions: DataFetchParams): string {
|
|
154
|
+
return JSON.stringify(dataFetchOptions, (_, value) => {
|
|
155
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
156
|
+
return Object.keys(value)
|
|
157
|
+
.sort()
|
|
158
|
+
.reduce((result: Record<string, unknown>, key) => {
|
|
159
|
+
result[key] = value[key];
|
|
160
|
+
return result;
|
|
161
|
+
}, {});
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function cache<T>(
|
|
168
|
+
fn: DataFetch<T>,
|
|
169
|
+
options?: CacheOptions,
|
|
170
|
+
): DataFetch<T> {
|
|
171
|
+
const {
|
|
172
|
+
tag = 'default',
|
|
173
|
+
maxAge = CacheTime.MINUTE * 5,
|
|
174
|
+
revalidate = 0,
|
|
175
|
+
onCache,
|
|
176
|
+
getKey,
|
|
177
|
+
} = options || {};
|
|
178
|
+
|
|
179
|
+
const tags = Array.isArray(tag) ? tag : [tag];
|
|
180
|
+
|
|
181
|
+
return async (dataFetchOptions) => {
|
|
182
|
+
// if downgrade, skip cache
|
|
183
|
+
if (dataFetchOptions.isDowngrade || !dataFetchOptions._id) {
|
|
184
|
+
return fn(dataFetchOptions);
|
|
185
|
+
}
|
|
186
|
+
const store = getLRUCache();
|
|
187
|
+
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
const storeKey = dataFetchOptions._id;
|
|
190
|
+
const cacheKey = getKey
|
|
191
|
+
? getKey(dataFetchOptions)
|
|
192
|
+
: generateKey(dataFetchOptions);
|
|
193
|
+
|
|
194
|
+
tags.forEach((t) => addTagKeyRelation(t, cacheKey));
|
|
195
|
+
|
|
196
|
+
let cacheStore = store.get(cacheKey);
|
|
197
|
+
if (!cacheStore) {
|
|
198
|
+
cacheStore = new Map();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const cached = cacheStore.get(storeKey);
|
|
202
|
+
if (cached) {
|
|
203
|
+
const age = now - cached.timestamp;
|
|
204
|
+
|
|
205
|
+
if (age < maxAge) {
|
|
206
|
+
if (onCache) {
|
|
207
|
+
const useCache = onCache({
|
|
208
|
+
status: 'hit',
|
|
209
|
+
key: cacheKey,
|
|
210
|
+
params: dataFetchOptions,
|
|
211
|
+
result: cached.data,
|
|
212
|
+
});
|
|
213
|
+
if (!useCache) {
|
|
214
|
+
return fn(dataFetchOptions);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return cached.data;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (revalidate > 0 && age < maxAge + revalidate) {
|
|
221
|
+
if (onCache) {
|
|
222
|
+
onCache({
|
|
223
|
+
status: 'stale',
|
|
224
|
+
key: cacheKey,
|
|
225
|
+
params: dataFetchOptions,
|
|
226
|
+
result: cached.data,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!cached.isRevalidating) {
|
|
231
|
+
cached.isRevalidating = true;
|
|
232
|
+
Promise.resolve().then(async () => {
|
|
233
|
+
try {
|
|
234
|
+
const newData = await fn(dataFetchOptions);
|
|
235
|
+
cacheStore!.set(storeKey, {
|
|
236
|
+
data: newData,
|
|
237
|
+
timestamp: Date.now(),
|
|
238
|
+
isRevalidating: false,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
store.set(cacheKey, cacheStore!);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
cached.isRevalidating = false;
|
|
244
|
+
console.error('Background revalidation failed:', error);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return cached.data;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const data = await fn(dataFetchOptions);
|
|
253
|
+
cacheStore.set(storeKey, {
|
|
254
|
+
data,
|
|
255
|
+
timestamp: now,
|
|
256
|
+
isRevalidating: false,
|
|
257
|
+
});
|
|
258
|
+
store.set(cacheKey, cacheStore);
|
|
259
|
+
|
|
260
|
+
if (onCache) {
|
|
261
|
+
onCache({
|
|
262
|
+
status: 'miss',
|
|
263
|
+
key: cacheKey,
|
|
264
|
+
params: dataFetchOptions,
|
|
265
|
+
result: data,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return data;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function revalidateTag(tag: string): void {
|
|
274
|
+
const tagKeyMap = getTagKeyMap();
|
|
275
|
+
const keys = tagKeyMap.get(tag);
|
|
276
|
+
const lruCache = getLRUCache();
|
|
277
|
+
if (keys) {
|
|
278
|
+
keys.forEach((key) => {
|
|
279
|
+
lruCache?.delete(key);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function clearStore(): void {
|
|
285
|
+
const lruCache = getLRUCache();
|
|
286
|
+
const tagKeyMap = getTagKeyMap();
|
|
287
|
+
|
|
288
|
+
lruCache?.clear();
|
|
289
|
+
delete globalThis.__MF_DATA_FETCH_CACHE__?.cacheStore;
|
|
290
|
+
tagKeyMap.clear();
|
|
291
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DATA_FETCH_FUNCTION } from '../constant';
|
|
2
|
+
import { dataFetchFunction } from './inject-data-fetch';
|
|
3
|
+
|
|
4
|
+
export async function callDataFetch() {
|
|
5
|
+
const dataFetch = globalThis[DATA_FETCH_FUNCTION];
|
|
6
|
+
if (dataFetch) {
|
|
7
|
+
await Promise.all(
|
|
8
|
+
dataFetch.map(async (options) => {
|
|
9
|
+
await dataFetchFunction(options);
|
|
10
|
+
}),
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|