@swoff/cli 0.3.9 → 0.3.10
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/__tests__/assemble-sw.test.js +8 -17
- package/dist/__tests__/assemble-sw.test.js.map +1 -1
- package/dist/__tests__/cli-integration.test.js +2 -2
- package/dist/__tests__/cli-integration.test.js.map +1 -1
- package/dist/__tests__/config-types.test.js +18 -8
- package/dist/__tests__/config-types.test.js.map +1 -1
- package/dist/__tests__/file-generators.test.js +3 -3
- package/dist/__tests__/file-generators.test.js.map +1 -1
- package/dist/__tests__/loader.test.js +5 -1
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/validator.test.js +6 -13
- package/dist/__tests__/validator.test.js.map +1 -1
- package/dist/index.js +39 -35
- package/dist/index.js.map +1 -1
- package/dist/lib/cli/help.js +13 -8
- package/dist/lib/cli/help.js.map +1 -1
- package/dist/lib/commands/add.js +38 -8
- package/dist/lib/commands/add.js.map +1 -1
- package/dist/lib/commands/assets.js +37 -0
- package/dist/lib/commands/assets.js.map +1 -0
- package/dist/lib/commands/clean.js +33 -17
- package/dist/lib/commands/clean.js.map +1 -1
- package/dist/lib/commands/generate-assets.js +44 -0
- package/dist/lib/commands/generate-assets.js.map +1 -0
- package/dist/lib/commands/generate-guide.js +22 -29
- package/dist/lib/commands/generate-guide.js.map +1 -1
- package/dist/lib/commands/generate.js +16 -9
- package/dist/lib/commands/generate.js.map +1 -1
- package/dist/lib/commands/info.js +36 -45
- package/dist/lib/commands/info.js.map +1 -1
- package/dist/lib/commands/init.js +25 -3
- package/dist/lib/commands/init.js.map +1 -1
- package/dist/lib/commands/validate.js +1 -1
- package/dist/lib/commands/validate.js.map +1 -1
- package/dist/lib/config/loader.js +25 -20
- package/dist/lib/config/loader.js.map +1 -1
- package/dist/lib/config/validator.js +20 -11
- package/dist/lib/config/validator.js.map +1 -1
- package/dist/lib/generators/asset-generator/dependency-resolver.js +76 -0
- package/dist/lib/generators/asset-generator/dependency-resolver.js.map +1 -0
- package/dist/lib/generators/asset-generator/generate.js +111 -0
- package/dist/lib/generators/asset-generator/generate.js.map +1 -0
- package/dist/lib/generators/asset-generator/guide.js +43 -0
- package/dist/lib/generators/asset-generator/guide.js.map +1 -0
- package/dist/lib/generators/asset-generator/html-patcher.js +65 -0
- package/dist/lib/generators/asset-generator/html-patcher.js.map +1 -0
- package/dist/lib/generators/asset-generator/ico-encoder.js +29 -0
- package/dist/lib/generators/asset-generator/ico-encoder.js.map +1 -0
- package/dist/lib/generators/asset-generator/manifest-patcher.js +23 -0
- package/dist/lib/generators/asset-generator/manifest-patcher.js.map +1 -0
- package/dist/lib/generators/asset-generator/maskable.js +15 -0
- package/dist/lib/generators/asset-generator/maskable.js.map +1 -0
- package/dist/lib/generators/asset-generator/rasterize.js +32 -0
- package/dist/lib/generators/asset-generator/rasterize.js.map +1 -0
- package/dist/lib/generators/asset-generator/sizes.js +25 -0
- package/dist/lib/generators/asset-generator/sizes.js.map +1 -0
- package/dist/lib/generators/file-generators/api-config.js +6 -0
- package/dist/lib/generators/file-generators/api-config.js.map +1 -0
- package/dist/lib/generators/file-generators/auth-state.js +2 -34
- package/dist/lib/generators/file-generators/auth-state.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-store.js +2 -250
- package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-user.js +3 -81
- package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
- package/dist/lib/generators/file-generators/background-sync.js +2 -57
- package/dist/lib/generators/file-generators/background-sync.js.map +1 -1
- package/dist/lib/generators/file-generators/cache.js +2 -30
- package/dist/lib/generators/file-generators/cache.js.map +1 -1
- package/dist/lib/generators/file-generators/client-injector.js +2 -130
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-state.js +6 -0
- package/dist/lib/generators/file-generators/fetch-state.js.map +1 -0
- package/dist/lib/generators/file-generators/fetch-wrapper.js +3 -295
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-framework-adapters.js +56 -0
- package/dist/lib/generators/file-generators/generate-framework-adapters.js.map +1 -0
- package/dist/lib/generators/file-generators/generate-hooks.js +4 -3
- package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
- package/dist/lib/generators/file-generators/gql-wrapper.js +3 -132
- package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/guide-generator.js +29 -1
- package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
- package/dist/lib/generators/file-generators/invalidation-tags.js +3 -258
- package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
- package/dist/lib/generators/file-generators/manifest.js +13 -3
- package/dist/lib/generators/file-generators/manifest.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-queue.js +3 -305
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-state.js +2 -117
- package/dist/lib/generators/file-generators/mutation-state.js.map +1 -1
- package/dist/lib/generators/file-generators/push.js +6 -138
- package/dist/lib/generators/file-generators/push.js.map +1 -1
- package/dist/lib/generators/file-generators/pwa-install.js +11 -84
- package/dist/lib/generators/file-generators/pwa-install.js.map +1 -1
- package/dist/lib/generators/file-generators/reset.js +6 -0
- package/dist/lib/generators/file-generators/reset.js.map +1 -0
- package/dist/lib/generators/file-generators/server-push.js +3 -134
- package/dist/lib/generators/file-generators/server-push.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-generator-build.js +4 -4
- package/dist/lib/generators/file-generators/sw-injector.js +9 -230
- package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/type-definitions.js +2 -111
- package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
- package/dist/lib/generators/sw-generator.js +5 -5
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js +25 -3
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/assemble-sw.js +13 -12
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/background-sync-handler.js +14 -7
- package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/config-header.js +1 -1
- package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
- package/dist/lib/generators/sw-sections/fetch-handler.js +165 -104
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/glob-matcher-code.js.map +1 -0
- package/dist/lib/generators/sw-sections/server-push-handler.js +12 -0
- package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/tag-management.js +6 -4
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +66 -35
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +89 -28
- package/dist/lib/shared/config-types.js.map +1 -1
- package/dist/runtime/api-config.js +18 -0
- package/dist/runtime/api-config.js.map +1 -0
- package/dist/runtime/auth-state.js +35 -0
- package/dist/runtime/auth-state.js.map +1 -0
- package/dist/runtime/auth-store.js +273 -0
- package/dist/runtime/auth-store.js.map +1 -0
- package/dist/runtime/auth-user.js +168 -0
- package/dist/runtime/auth-user.js.map +1 -0
- package/dist/runtime/background-sync.js +59 -0
- package/dist/runtime/background-sync.js.map +1 -0
- package/dist/runtime/cache.js +30 -0
- package/dist/runtime/cache.js.map +1 -0
- package/dist/runtime/client-injector.js +136 -0
- package/dist/runtime/client-injector.js.map +1 -0
- package/dist/runtime/fetch-state.js +42 -0
- package/dist/runtime/fetch-state.js.map +1 -0
- package/dist/runtime/fetch-wrapper.js +355 -0
- package/dist/runtime/fetch-wrapper.js.map +1 -0
- package/dist/runtime/gql-wrapper.js +135 -0
- package/dist/runtime/gql-wrapper.js.map +1 -0
- package/dist/runtime/invalidation-tags.js +260 -0
- package/dist/runtime/invalidation-tags.js.map +1 -0
- package/dist/runtime/mutation-queue.js +304 -0
- package/dist/runtime/mutation-queue.js.map +1 -0
- package/dist/runtime/mutation-state.js +117 -0
- package/dist/runtime/mutation-state.js.map +1 -0
- package/dist/runtime/push.js +138 -0
- package/dist/runtime/push.js.map +1 -0
- package/dist/runtime/pwa-index.js +11 -0
- package/dist/runtime/pwa-index.js.map +1 -0
- package/dist/runtime/pwa-injector.js +44 -0
- package/dist/runtime/pwa-injector.js.map +1 -0
- package/dist/runtime/pwa-install.js +83 -0
- package/dist/runtime/pwa-install.js.map +1 -0
- package/dist/runtime/pwa-prompt.js +54 -0
- package/dist/runtime/pwa-prompt.js.map +1 -0
- package/dist/runtime/reset.js +148 -0
- package/dist/runtime/reset.js.map +1 -0
- package/dist/runtime/server-push.js +148 -0
- package/dist/runtime/server-push.js.map +1 -0
- package/dist/runtime/sw-injector.js +236 -0
- package/dist/runtime/sw-injector.js.map +1 -0
- package/dist/runtime/type-definitions.js +138 -0
- package/dist/runtime/type-definitions.js.map +1 -0
- package/dist/runtime/utils.js +27 -0
- package/dist/runtime/utils.js.map +1 -0
- package/package.json +3 -2
- package/templates/react/useAuth.jsx +106 -0
- package/templates/react/useAuth.tsx +118 -0
- package/templates/{hooks → react}/useBackgroundSync.jsx +6 -4
- package/templates/{hooks → react}/useBackgroundSync.tsx +6 -4
- package/templates/{hooks → react}/useCacheInvalidation.jsx +2 -2
- package/templates/react/useCacheInvalidation.tsx +52 -0
- package/templates/react/useCachedFetch.jsx +140 -0
- package/templates/react/useCachedFetch.tsx +160 -0
- package/templates/react/useIsFetching.jsx +32 -0
- package/templates/react/useIsFetching.tsx +32 -0
- package/templates/react/useMutation.jsx +141 -0
- package/templates/react/useMutation.tsx +180 -0
- package/templates/react/useMutationQueue.jsx +69 -0
- package/templates/react/useMutationQueue.tsx +83 -0
- package/templates/react/useNetworkStatus.jsx +61 -0
- package/templates/react/useNetworkStatus.tsx +76 -0
- package/templates/react/usePrefetch.jsx +32 -0
- package/templates/react/usePrefetch.tsx +42 -0
- package/templates/{hooks → react}/usePushSubscription.jsx +4 -4
- package/templates/{hooks → react}/usePushSubscription.tsx +4 -4
- package/templates/{hooks → react}/useSWUpdate.jsx +3 -3
- package/templates/{hooks → react}/useSWUpdate.tsx +4 -4
- package/templates/react/useSwoffReset.jsx +32 -0
- package/templates/react/useSwoffReset.tsx +39 -0
- package/docs/API.md +0 -565
- package/docs/ARCHITECTURE.md +0 -374
- package/docs/CLI.md +0 -148
- package/docs/COMPARISON.md +0 -192
- package/docs/CONFIG.md +0 -277
- package/docs/ECOSYSTEM.md +0 -33
- package/templates/hooks/useAuth.jsx +0 -30
- package/templates/hooks/useAuth.tsx +0 -30
- package/templates/hooks/useCacheInvalidation.tsx +0 -19
- package/templates/hooks/useCachedFetch.jsx +0 -69
- package/templates/hooks/useCachedFetch.tsx +0 -81
- package/templates/hooks/useMutation.jsx +0 -76
- package/templates/hooks/useMutation.tsx +0 -109
- package/templates/hooks/useMutationQueue.jsx +0 -43
- package/templates/hooks/useMutationQueue.tsx +0 -50
- package/templates/hooks/useNetworkStatus.jsx +0 -19
- package/templates/hooks/useNetworkStatus.tsx +0 -19
- package/templates/hooks/usePrefetch.jsx +0 -10
- package/templates/hooks/usePrefetch.tsx +0 -25
- /package/templates/{hooks → react}/useMutationState.jsx +0 -0
- /package/templates/{hooks → react}/useMutationState.tsx +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { getAuthState } from "../auth/state.js";
|
|
3
|
+
import { setAuth, clearAuth, ensureValidAuth } from "../auth/store.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reactive auth state with actions: setAuth, clearAuth, ensureValid.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const { authenticated, user, online, isLoading, error, setAuth, clearAuth, ensureValid } = useAuth();
|
|
10
|
+
*
|
|
11
|
+
* // Login — call setAuth with the response from your login endpoint:
|
|
12
|
+
* const res = await fetch("/api/login", { method: "POST", body });
|
|
13
|
+
* const data = await res.json();
|
|
14
|
+
* await setAuth({ token: data.token, user: data.user, expiresAt: data.expiresAt });
|
|
15
|
+
*
|
|
16
|
+
* // Logout:
|
|
17
|
+
* await clearAuth();
|
|
18
|
+
*
|
|
19
|
+
* // Silent session restore:
|
|
20
|
+
* const auth = await ensureValid();
|
|
21
|
+
*/
|
|
22
|
+
export function useAuth() {
|
|
23
|
+
const [state, setState] = useState(() => ({
|
|
24
|
+
authenticated: false,
|
|
25
|
+
user: null,
|
|
26
|
+
online: typeof navigator !== "undefined" ? navigator.onLine : true,
|
|
27
|
+
isLoading: false,
|
|
28
|
+
error: null,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const refreshState = useCallback(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const authState = await getAuthState();
|
|
34
|
+
setState((s) => ({ ...s, ...authState, error: null }));
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
refreshState();
|
|
42
|
+
|
|
43
|
+
const onOnline = () => setState((s) => ({ ...s, online: true }));
|
|
44
|
+
const onOffline = () => setState((s) => ({ ...s, online: false }));
|
|
45
|
+
const onAuthChange = () => refreshState();
|
|
46
|
+
|
|
47
|
+
window.addEventListener("online", onOnline);
|
|
48
|
+
window.addEventListener("offline", onOffline);
|
|
49
|
+
window.addEventListener("sw-auth-state-change", onAuthChange);
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
window.removeEventListener("online", onOnline);
|
|
53
|
+
window.removeEventListener("offline", onOffline);
|
|
54
|
+
window.removeEventListener("sw-auth-state-change", onAuthChange);
|
|
55
|
+
};
|
|
56
|
+
}, [refreshState]);
|
|
57
|
+
|
|
58
|
+
const doSetAuth = useCallback(async (authData) => {
|
|
59
|
+
setState((s) => ({ ...s, isLoading: true, error: null }));
|
|
60
|
+
try {
|
|
61
|
+
await setAuth(authData);
|
|
62
|
+
await refreshState();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
setState((s) => ({ ...s, error: err instanceof Error ? err : new Error(String(err)) }));
|
|
65
|
+
} finally {
|
|
66
|
+
setState((s) => ({ ...s, isLoading: false }));
|
|
67
|
+
}
|
|
68
|
+
}, [refreshState]);
|
|
69
|
+
|
|
70
|
+
const doClearAuth = useCallback(async () => {
|
|
71
|
+
setState((s) => ({ ...s, isLoading: true, error: null }));
|
|
72
|
+
try {
|
|
73
|
+
await clearAuth();
|
|
74
|
+
await refreshState();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
setState((s) => ({ ...s, error: err instanceof Error ? err : new Error(String(err)) }));
|
|
77
|
+
} finally {
|
|
78
|
+
setState((s) => ({ ...s, isLoading: false }));
|
|
79
|
+
}
|
|
80
|
+
}, [refreshState]);
|
|
81
|
+
|
|
82
|
+
const doEnsureValid = useCallback(async () => {
|
|
83
|
+
setState((s) => ({ ...s, isLoading: true, error: null }));
|
|
84
|
+
try {
|
|
85
|
+
const auth = await ensureValidAuth();
|
|
86
|
+
await refreshState();
|
|
87
|
+
return auth;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
setState((s) => ({ ...s, error: err instanceof Error ? err : new Error(String(err)) }));
|
|
90
|
+
return null;
|
|
91
|
+
} finally {
|
|
92
|
+
setState((s) => ({ ...s, isLoading: false }));
|
|
93
|
+
}
|
|
94
|
+
}, [refreshState]);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
authenticated: state.authenticated,
|
|
98
|
+
user: state.user,
|
|
99
|
+
online: state.online,
|
|
100
|
+
isLoading: state.isLoading,
|
|
101
|
+
error: state.error,
|
|
102
|
+
setAuth: doSetAuth,
|
|
103
|
+
clearAuth: doClearAuth,
|
|
104
|
+
ensureValid: doEnsureValid,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { getAuthState } from "../auth/state.ts";
|
|
3
|
+
import { setAuth, clearAuth, ensureValidAuth } from "../auth/store.ts";
|
|
4
|
+
import type { AuthData } from "../auth/store.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reactive auth state with actions: setAuth, clearAuth, ensureValid.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const { authenticated, user, online, isLoading, error, setAuth, clearAuth, ensureValid } = useAuth();
|
|
11
|
+
*
|
|
12
|
+
* // Login — call setAuth with the response from your login endpoint:
|
|
13
|
+
* const res = await fetch("/api/login", { method: "POST", body });
|
|
14
|
+
* const data = await res.json();
|
|
15
|
+
* await setAuth({ token: data.token, user: data.user, expiresAt: data.expiresAt });
|
|
16
|
+
*
|
|
17
|
+
* // Logout — clears tokens from memory and user cache from IndexedDB:
|
|
18
|
+
* await clearAuth();
|
|
19
|
+
*
|
|
20
|
+
* // Silently refresh the session (page restore) — wraps ensureValidAuth():
|
|
21
|
+
* const auth = await ensureValid();
|
|
22
|
+
*
|
|
23
|
+
* Auth behavior by type:
|
|
24
|
+
* - Cookie auth: credentials ("include") sent automatically by the browser.
|
|
25
|
+
* setAuth still caches user data for offline display.
|
|
26
|
+
* - Bearer auth: token lives in memory only. clearAuth wipes it.
|
|
27
|
+
* ensureValid tries a silent refresh via refreshSession() in auth/user.ts.
|
|
28
|
+
*
|
|
29
|
+
* @returns {{ authenticated, user, online, isLoading, error, setAuth, clearAuth, ensureValid }}
|
|
30
|
+
*/
|
|
31
|
+
export function useAuth() {
|
|
32
|
+
const [state, setState] = useState(() => ({
|
|
33
|
+
authenticated: false,
|
|
34
|
+
user: null as Record<string, unknown> | null,
|
|
35
|
+
online: typeof navigator !== "undefined" ? navigator.onLine : true,
|
|
36
|
+
isLoading: false,
|
|
37
|
+
error: null as Error | null,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const refreshState = useCallback(async () => {
|
|
41
|
+
try {
|
|
42
|
+
const authState = await getAuthState();
|
|
43
|
+
setState((s) => ({ ...s, ...authState, error: null }));
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore — auth not initialized
|
|
46
|
+
}
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
refreshState();
|
|
51
|
+
|
|
52
|
+
const onOnline = () => setState((s) => ({ ...s, online: true }));
|
|
53
|
+
const onOffline = () => setState((s) => ({ ...s, online: false }));
|
|
54
|
+
const onAuthChange = () => refreshState();
|
|
55
|
+
|
|
56
|
+
window.addEventListener("online", onOnline);
|
|
57
|
+
window.addEventListener("offline", onOffline);
|
|
58
|
+
window.addEventListener("sw-auth-state-change", onAuthChange);
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
window.removeEventListener("online", onOnline);
|
|
62
|
+
window.removeEventListener("offline", onOffline);
|
|
63
|
+
window.removeEventListener("sw-auth-state-change", onAuthChange);
|
|
64
|
+
};
|
|
65
|
+
}, [refreshState]);
|
|
66
|
+
|
|
67
|
+
const doSetAuth = useCallback(async (authData: AuthData) => {
|
|
68
|
+
setState((s) => ({ ...s, isLoading: true, error: null }));
|
|
69
|
+
try {
|
|
70
|
+
await setAuth(authData);
|
|
71
|
+
await refreshState();
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
74
|
+
setState((s) => ({ ...s, error }));
|
|
75
|
+
} finally {
|
|
76
|
+
setState((s) => ({ ...s, isLoading: false }));
|
|
77
|
+
}
|
|
78
|
+
}, [refreshState]);
|
|
79
|
+
|
|
80
|
+
const doClearAuth = useCallback(async () => {
|
|
81
|
+
setState((s) => ({ ...s, isLoading: true, error: null }));
|
|
82
|
+
try {
|
|
83
|
+
await clearAuth();
|
|
84
|
+
await refreshState();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
87
|
+
setState((s) => ({ ...s, error }));
|
|
88
|
+
} finally {
|
|
89
|
+
setState((s) => ({ ...s, isLoading: false }));
|
|
90
|
+
}
|
|
91
|
+
}, [refreshState]);
|
|
92
|
+
|
|
93
|
+
const doEnsureValid = useCallback(async (): Promise<AuthData | null> => {
|
|
94
|
+
setState((s) => ({ ...s, isLoading: true, error: null }));
|
|
95
|
+
try {
|
|
96
|
+
const auth = await ensureValidAuth();
|
|
97
|
+
await refreshState();
|
|
98
|
+
return auth;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
101
|
+
setState((s) => ({ ...s, error }));
|
|
102
|
+
return null;
|
|
103
|
+
} finally {
|
|
104
|
+
setState((s) => ({ ...s, isLoading: false }));
|
|
105
|
+
}
|
|
106
|
+
}, [refreshState]);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
authenticated: state.authenticated,
|
|
110
|
+
user: state.user,
|
|
111
|
+
online: state.online,
|
|
112
|
+
isLoading: state.isLoading,
|
|
113
|
+
error: state.error,
|
|
114
|
+
setAuth: doSetAuth,
|
|
115
|
+
clearAuth: doClearAuth,
|
|
116
|
+
ensureValid: doEnsureValid,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import { retrySync } from "../
|
|
2
|
+
import { retrySync } from "../offline/sync.js";
|
|
3
3
|
|
|
4
4
|
export function useBackgroundSync() {
|
|
5
|
-
const [state, setState] = useState({
|
|
6
|
-
supported: "
|
|
5
|
+
const [state, setState] = useState(() => ({
|
|
6
|
+
supported: typeof window !== "undefined" && typeof navigator !== "undefined"
|
|
7
|
+
? "serviceWorker" in navigator && "SyncManager" in window
|
|
8
|
+
: false,
|
|
7
9
|
registered: false,
|
|
8
10
|
lastSync: null,
|
|
9
|
-
});
|
|
11
|
+
}));
|
|
10
12
|
|
|
11
13
|
useEffect(() => {
|
|
12
14
|
const onSyncComplete = (e) => {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import { retrySync } from "../
|
|
2
|
+
import { retrySync } from "../offline/sync.ts";
|
|
3
3
|
|
|
4
4
|
export function useBackgroundSync() {
|
|
5
|
-
const [state, setState] = useState({
|
|
6
|
-
supported: "
|
|
5
|
+
const [state, setState] = useState(() => ({
|
|
6
|
+
supported: typeof window !== "undefined" && typeof navigator !== "undefined"
|
|
7
|
+
? "serviceWorker" in navigator && "SyncManager" in window
|
|
8
|
+
: false,
|
|
7
9
|
registered: false,
|
|
8
10
|
lastSync: null as { succeeded: number; failed: number } | null,
|
|
9
|
-
});
|
|
11
|
+
}));
|
|
10
12
|
|
|
11
13
|
useEffect(() => {
|
|
12
14
|
const onSyncComplete = (e: CustomEvent) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
-
import { invalidateByTag, invalidateByTags } from "../cache.js";
|
|
3
|
-
import { invalidateUrl } from "../
|
|
2
|
+
import { invalidateByTag, invalidateByTags } from "../cache/index.js";
|
|
3
|
+
import { invalidateUrl } from "../cache/tags.js";
|
|
4
4
|
|
|
5
5
|
export function useCacheInvalidation() {
|
|
6
6
|
const invalidateTag = useCallback(async (tag) => {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { invalidateByTag, invalidateByTags } from "../cache/index.ts";
|
|
3
|
+
import { invalidateUrl } from "../cache/tags.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook exposing cache invalidation functions for manual control.
|
|
7
|
+
*
|
|
8
|
+
* Useful when you need to invalidate the cache after a mutation that wasn't
|
|
9
|
+
* automatically handled (e.g., a third-party API, a non-fetchWithCache call).
|
|
10
|
+
*
|
|
11
|
+
* Invalidation triggers:
|
|
12
|
+
* - The service worker refetches cached URLs for the invalidated tags
|
|
13
|
+
* - Active useCachedFetch hooks for those URLs auto-refetch
|
|
14
|
+
* - The "cache-invalidated" CustomEvent is dispatched on window
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const { invalidateByTag, invalidateByTags, invalidateUrl } = useCacheInvalidation();
|
|
18
|
+
*
|
|
19
|
+
* // Invalidate by a single tag
|
|
20
|
+
* await invalidateByTag("todos");
|
|
21
|
+
*
|
|
22
|
+
* // Invalidate by multiple tags at once
|
|
23
|
+
* await invalidateByTags(["todos", "projects"]);
|
|
24
|
+
*
|
|
25
|
+
* // Invalidate a specific URL
|
|
26
|
+
* await invalidateUrl("/api/todos");
|
|
27
|
+
*
|
|
28
|
+
* Auth behavior:
|
|
29
|
+
* Invalidation is auth-agnostic. If the refetched URL requires auth headers,
|
|
30
|
+
* the service worker's refetch (from tag-management.ts) will replay the
|
|
31
|
+
* original request's method and body. For GET requests, no auth headers are
|
|
32
|
+
* sent during invalidation-based refetch — this is correct for cookie auth
|
|
33
|
+
* (browser sends cookies automatically), but for bearer auth the user
|
|
34
|
+
* endpoint fetch is handled by refreshSession/fetchCurrentUser in auth/user.ts.
|
|
35
|
+
*
|
|
36
|
+
* @returns {{ invalidateByTag, invalidateByTags, invalidateUrl }}
|
|
37
|
+
*/
|
|
38
|
+
export function useCacheInvalidation() {
|
|
39
|
+
const invalidateTag = useCallback(async (tag: string) => {
|
|
40
|
+
await invalidateByTag(tag);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const invalidateTags = useCallback(async (tags: string[]) => {
|
|
44
|
+
await invalidateByTags(tags);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const invalidateCacheUrl = useCallback(async (url: string) => {
|
|
48
|
+
await invalidateUrl(url);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
return { invalidateByTag: invalidateTag, invalidateByTags: invalidateTags, invalidateUrl: invalidateCacheUrl };
|
|
52
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { fetchWithCache } from "../fetch/core.js";
|
|
3
|
+
import { generateTags } from "../cache/tags.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetch and cache data with automatic invalidation, revalidation, and offline support.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const { data, error, loading, refetch } = useCachedFetch("/api/todos");
|
|
10
|
+
*
|
|
11
|
+
* // Select/transform data
|
|
12
|
+
* const { data: names } = useCachedFetch("/api/users", {
|
|
13
|
+
* select: (users) => users.map(u => u.name),
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // Keep previous data while fetching next page
|
|
17
|
+
* const { data } = useCachedFetch(`/api/items?page=${page}`, {
|
|
18
|
+
* keepPreviousData: true,
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Retry on failure
|
|
22
|
+
* const { data } = useCachedFetch("/api/todos", { retry: 3 });
|
|
23
|
+
*
|
|
24
|
+
* // Placeholder data while loading
|
|
25
|
+
* const { data } = useCachedFetch("/api/todos", {
|
|
26
|
+
* placeholderData: { items: [] },
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
export function useCachedFetch(url, options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
select,
|
|
32
|
+
retry: retryOpt,
|
|
33
|
+
placeholderData,
|
|
34
|
+
onSuccess,
|
|
35
|
+
onError,
|
|
36
|
+
...fetchOptions
|
|
37
|
+
} = options;
|
|
38
|
+
|
|
39
|
+
const [data, setData] = useState(placeholderData ?? null);
|
|
40
|
+
const [error, setError] = useState(null);
|
|
41
|
+
const [loading, setLoading] = useState(
|
|
42
|
+
() => url != null && options.enabled !== false,
|
|
43
|
+
);
|
|
44
|
+
const [refetchCount, setRefetchCount] = useState(0);
|
|
45
|
+
|
|
46
|
+
const cachedRef = useRef(placeholderData ?? null);
|
|
47
|
+
const prevSelectedRef = useRef(null);
|
|
48
|
+
const onSuccessRef = useRef(onSuccess);
|
|
49
|
+
const onErrorRef = useRef(onError);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
onSuccessRef.current = onSuccess;
|
|
53
|
+
onErrorRef.current = onError;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const refetch = useCallback(() => setRefetchCount((c) => c + 1), []);
|
|
57
|
+
|
|
58
|
+
const isEnabled = options.enabled !== false && url != null;
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!isEnabled) return;
|
|
62
|
+
|
|
63
|
+
let cancelled = false;
|
|
64
|
+
let retriesLeft =
|
|
65
|
+
retryOpt === true
|
|
66
|
+
? Infinity
|
|
67
|
+
: typeof retryOpt === "number"
|
|
68
|
+
? retryOpt
|
|
69
|
+
: 0;
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
|
|
72
|
+
const doFetch = async () => {
|
|
73
|
+
setLoading(true);
|
|
74
|
+
try {
|
|
75
|
+
const { response } = await fetchWithCache(url, {
|
|
76
|
+
...fetchOptions,
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
});
|
|
79
|
+
if (cancelled) return;
|
|
80
|
+
let selected = null;
|
|
81
|
+
if (response) {
|
|
82
|
+
const raw = await response.json();
|
|
83
|
+
selected = select ? select(raw) : raw;
|
|
84
|
+
setData(selected);
|
|
85
|
+
cachedRef.current = selected;
|
|
86
|
+
prevSelectedRef.current = selected;
|
|
87
|
+
if (!cancelled) onSuccessRef.current?.(selected);
|
|
88
|
+
} else {
|
|
89
|
+
setData(null);
|
|
90
|
+
cachedRef.current = null;
|
|
91
|
+
prevSelectedRef.current = null;
|
|
92
|
+
}
|
|
93
|
+
if (!cancelled) setError(null);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (
|
|
96
|
+
!cancelled &&
|
|
97
|
+
err instanceof DOMException &&
|
|
98
|
+
err.name === "AbortError"
|
|
99
|
+
)
|
|
100
|
+
return;
|
|
101
|
+
if (!cancelled && retriesLeft > 0) {
|
|
102
|
+
retriesLeft--;
|
|
103
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
104
|
+
return doFetch();
|
|
105
|
+
}
|
|
106
|
+
if (!cancelled) {
|
|
107
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
108
|
+
setError(e);
|
|
109
|
+
onErrorRef.current?.(e);
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
if (!cancelled) setLoading(false);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
doFetch();
|
|
117
|
+
return () => {
|
|
118
|
+
cancelled = true;
|
|
119
|
+
controller.abort();
|
|
120
|
+
};
|
|
121
|
+
}, [url, refetchCount, isEnabled]);
|
|
122
|
+
|
|
123
|
+
// Auto-refetch on cache invalidation
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!isEnabled) return;
|
|
126
|
+
const onInvalidated = (e) => {
|
|
127
|
+
const detail = e.detail;
|
|
128
|
+
const tags = detail?.tags;
|
|
129
|
+
if (!tags || !Array.isArray(tags)) return;
|
|
130
|
+
const urlTags = generateTags(url);
|
|
131
|
+
if (tags.some((t) => urlTags.includes(t))) {
|
|
132
|
+
setRefetchCount((c) => c + 1);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
window.addEventListener("cache-invalidated", onInvalidated);
|
|
136
|
+
return () => window.removeEventListener("cache-invalidated", onInvalidated);
|
|
137
|
+
}, [url, isEnabled]);
|
|
138
|
+
|
|
139
|
+
return { data, error, loading, refetch };
|
|
140
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { fetchWithCache } from "../fetch/core.ts";
|
|
3
|
+
import { generateTags } from "../cache/tags.ts";
|
|
4
|
+
import type { FetchWithCacheOptions } from "../fetch/core.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fetch and cache data with automatic invalidation, revalidation, and offline support.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const { data, error, loading, refetch } = useCachedFetch<MyType>("/api/todos");
|
|
11
|
+
*
|
|
12
|
+
* // Select/transform data — skips re-render if selected value hasn't changed
|
|
13
|
+
* const { data: names } = useCachedFetch("/api/users", {
|
|
14
|
+
* select: (users) => users.map(u => u.name),
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // Keep previous data while fetching the next page
|
|
18
|
+
* const { data } = useCachedFetch(`/api/items?page=${page}`, {
|
|
19
|
+
* keepPreviousData: true,
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Retry on failure (default 0 retries)
|
|
23
|
+
* const { data } = useCachedFetch("/api/todos", { retry: 3 });
|
|
24
|
+
*
|
|
25
|
+
* // Placeholder data while loading for the first time
|
|
26
|
+
* const { data } = useCachedFetch("/api/todos", {
|
|
27
|
+
* placeholderData: { items: [] },
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // Callbacks on success/error
|
|
31
|
+
* const { data, refetch } = useCachedFetch("/api/todos", {
|
|
32
|
+
* onSuccess: (data) => console.log("Loaded", data),
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* @param url - URL to fetch. Pass null to skip fetching.
|
|
36
|
+
* @param options - Fetch options plus: select, keepPreviousData, retry, placeholderData, onSuccess, onError
|
|
37
|
+
*/
|
|
38
|
+
export function useCachedFetch<T = unknown, R = T>(
|
|
39
|
+
url: string | null,
|
|
40
|
+
options: FetchWithCacheOptions & {
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
select?: (data: T) => R;
|
|
43
|
+
keepPreviousData?: boolean;
|
|
44
|
+
retry?: number | boolean;
|
|
45
|
+
placeholderData?: R;
|
|
46
|
+
onSuccess?: (data: R) => void;
|
|
47
|
+
onError?: (error: Error) => void;
|
|
48
|
+
} = {},
|
|
49
|
+
) {
|
|
50
|
+
const {
|
|
51
|
+
select,
|
|
52
|
+
retry: retryOpt,
|
|
53
|
+
placeholderData,
|
|
54
|
+
onSuccess,
|
|
55
|
+
onError,
|
|
56
|
+
...fetchOptions
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
const [data, setData] = useState<R | null>(placeholderData ?? null);
|
|
60
|
+
const [error, setError] = useState<Error | null>(null);
|
|
61
|
+
const [loading, setLoading] = useState(
|
|
62
|
+
() => url != null && options.enabled !== false,
|
|
63
|
+
);
|
|
64
|
+
const [refetchCount, setRefetchCount] = useState(0);
|
|
65
|
+
|
|
66
|
+
const cachedRef = useRef<R | null>(placeholderData ?? null);
|
|
67
|
+
const prevSelectedRef = useRef<R | null>(null);
|
|
68
|
+
const onSuccessRef = useRef(onSuccess);
|
|
69
|
+
const onErrorRef = useRef(onError);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
onSuccessRef.current = onSuccess;
|
|
73
|
+
onErrorRef.current = onError;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const refetch = useCallback(() => setRefetchCount((c) => c + 1), []);
|
|
77
|
+
|
|
78
|
+
const isEnabled = options.enabled !== false && url != null;
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!isEnabled) return;
|
|
82
|
+
|
|
83
|
+
let cancelled = false;
|
|
84
|
+
let retriesLeft: number =
|
|
85
|
+
retryOpt === true
|
|
86
|
+
? Infinity
|
|
87
|
+
: typeof retryOpt === "number"
|
|
88
|
+
? retryOpt
|
|
89
|
+
: 0;
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
|
|
92
|
+
const doFetch = async (): Promise<void> => {
|
|
93
|
+
setLoading(true);
|
|
94
|
+
try {
|
|
95
|
+
const { response } = await fetchWithCache(url, {
|
|
96
|
+
...fetchOptions,
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
if (cancelled) return;
|
|
100
|
+
let selected: R | null = null;
|
|
101
|
+
if (response) {
|
|
102
|
+
const raw: T = await response.json();
|
|
103
|
+
selected = select ? select(raw) : (raw as unknown as R);
|
|
104
|
+
setData(selected);
|
|
105
|
+
cachedRef.current = selected;
|
|
106
|
+
prevSelectedRef.current = selected;
|
|
107
|
+
if (!cancelled) onSuccessRef.current?.(selected);
|
|
108
|
+
} else {
|
|
109
|
+
setData(null);
|
|
110
|
+
cachedRef.current = null;
|
|
111
|
+
prevSelectedRef.current = null;
|
|
112
|
+
}
|
|
113
|
+
if (!cancelled) setError(null);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (
|
|
116
|
+
!cancelled &&
|
|
117
|
+
err instanceof DOMException &&
|
|
118
|
+
err.name === "AbortError"
|
|
119
|
+
)
|
|
120
|
+
return;
|
|
121
|
+
if (!cancelled && retriesLeft > 0) {
|
|
122
|
+
retriesLeft--;
|
|
123
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
124
|
+
return doFetch();
|
|
125
|
+
}
|
|
126
|
+
if (!cancelled) {
|
|
127
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
128
|
+
setError(e);
|
|
129
|
+
onErrorRef.current?.(e);
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
if (!cancelled) setLoading(false);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
doFetch();
|
|
137
|
+
return () => {
|
|
138
|
+
cancelled = true;
|
|
139
|
+
controller.abort();
|
|
140
|
+
};
|
|
141
|
+
}, [url, refetchCount, isEnabled]);
|
|
142
|
+
|
|
143
|
+
// Auto-refetch on cache invalidation
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!isEnabled) return;
|
|
146
|
+
const onInvalidated = (e: Event) => {
|
|
147
|
+
const detail = (e as CustomEvent).detail;
|
|
148
|
+
const tags = detail?.tags;
|
|
149
|
+
if (!tags || !Array.isArray(tags)) return;
|
|
150
|
+
const urlTags = generateTags(url!);
|
|
151
|
+
if (tags.some((t: string) => urlTags.includes(t))) {
|
|
152
|
+
setRefetchCount((c) => c + 1);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
window.addEventListener("cache-invalidated", onInvalidated);
|
|
156
|
+
return () => window.removeEventListener("cache-invalidated", onInvalidated);
|
|
157
|
+
}, [url, isEnabled]);
|
|
158
|
+
|
|
159
|
+
return { data, error, loading, refetch };
|
|
160
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { getFetchCount } from "../fetch/state";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook that returns true when any swoff fetch or mutation is in-flight.
|
|
6
|
+
* Uses a global counter incremented/decremented by fetchWithCache.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const isFetching = useIsFetching();
|
|
10
|
+
*
|
|
11
|
+
* return (
|
|
12
|
+
* <>
|
|
13
|
+
* {isFetching && <Spinner />}
|
|
14
|
+
* <MainContent />
|
|
15
|
+
* </>
|
|
16
|
+
* );
|
|
17
|
+
*/
|
|
18
|
+
export function useIsFetching() {
|
|
19
|
+
const [isFetching, setIsFetching] = useState(() => getFetchCount() > 0);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const onChange = (e) => {
|
|
23
|
+
setIsFetching(e.detail.count > 0);
|
|
24
|
+
};
|
|
25
|
+
window.addEventListener("fetch-count-changed", onChange);
|
|
26
|
+
return () => {
|
|
27
|
+
window.removeEventListener("fetch-count-changed", onChange);
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
return isFetching;
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { getFetchCount } from "../fetch/state";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook that returns true when any swoff fetch or mutation is in-flight.
|
|
6
|
+
* Uses a global counter incremented/decremented by fetchWithCache.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const isFetching = useIsFetching();
|
|
10
|
+
*
|
|
11
|
+
* return (
|
|
12
|
+
* <>
|
|
13
|
+
* {isFetching && <Spinner />}
|
|
14
|
+
* <MainContent />
|
|
15
|
+
* </>
|
|
16
|
+
* );
|
|
17
|
+
*/
|
|
18
|
+
export function useIsFetching(): boolean {
|
|
19
|
+
const [isFetching, setIsFetching] = useState(() => getFetchCount() > 0);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const onChange = (e: CustomEvent<{ count: number }>) => {
|
|
23
|
+
setIsFetching(e.detail.count > 0);
|
|
24
|
+
};
|
|
25
|
+
window.addEventListener("fetch-count-changed", onChange as EventListener);
|
|
26
|
+
return () => {
|
|
27
|
+
window.removeEventListener("fetch-count-changed", onChange as EventListener);
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
return isFetching;
|
|
32
|
+
}
|