@swoff/cli 0.3.8 → 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.
Files changed (228) hide show
  1. package/README.md +47 -33
  2. package/dist/__tests__/assemble-sw.test.js +14 -53
  3. package/dist/__tests__/assemble-sw.test.js.map +1 -1
  4. package/dist/__tests__/cli-integration.test.js +119 -11
  5. package/dist/__tests__/cli-integration.test.js.map +1 -1
  6. package/dist/__tests__/config-types.test.js +44 -17
  7. package/dist/__tests__/config-types.test.js.map +1 -1
  8. package/dist/__tests__/file-generators.test.js +3 -3
  9. package/dist/__tests__/file-generators.test.js.map +1 -1
  10. package/dist/__tests__/glob-matcher.test.js +86 -0
  11. package/dist/__tests__/glob-matcher.test.js.map +1 -0
  12. package/dist/__tests__/invalidation-tags.test.js +207 -23
  13. package/dist/__tests__/invalidation-tags.test.js.map +1 -1
  14. package/dist/__tests__/loader.test.js +16 -12
  15. package/dist/__tests__/loader.test.js.map +1 -1
  16. package/dist/__tests__/validator.test.js +140 -100
  17. package/dist/__tests__/validator.test.js.map +1 -1
  18. package/dist/index.js +39 -34
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/cli/help.js +12 -12
  21. package/dist/lib/cli/help.js.map +1 -1
  22. package/dist/lib/commands/add.js +84 -36
  23. package/dist/lib/commands/add.js.map +1 -1
  24. package/dist/lib/commands/assets.js +37 -0
  25. package/dist/lib/commands/assets.js.map +1 -0
  26. package/dist/lib/commands/clean.js +32 -30
  27. package/dist/lib/commands/clean.js.map +1 -1
  28. package/dist/lib/commands/generate-assets.js +44 -0
  29. package/dist/lib/commands/generate-assets.js.map +1 -0
  30. package/dist/lib/commands/generate-guide.js +22 -29
  31. package/dist/lib/commands/generate-guide.js.map +1 -1
  32. package/dist/lib/commands/generate.js +17 -24
  33. package/dist/lib/commands/generate.js.map +1 -1
  34. package/dist/lib/commands/info.js +41 -48
  35. package/dist/lib/commands/info.js.map +1 -1
  36. package/dist/lib/commands/init.js +27 -13
  37. package/dist/lib/commands/init.js.map +1 -1
  38. package/dist/lib/commands/validate.js +4 -4
  39. package/dist/lib/commands/validate.js.map +1 -1
  40. package/dist/lib/config/loader.js +25 -23
  41. package/dist/lib/config/loader.js.map +1 -1
  42. package/dist/lib/config/validator.js +169 -91
  43. package/dist/lib/config/validator.js.map +1 -1
  44. package/dist/lib/generators/asset-generator/dependency-resolver.js +76 -0
  45. package/dist/lib/generators/asset-generator/dependency-resolver.js.map +1 -0
  46. package/dist/lib/generators/asset-generator/generate.js +111 -0
  47. package/dist/lib/generators/asset-generator/generate.js.map +1 -0
  48. package/dist/lib/generators/asset-generator/guide.js +43 -0
  49. package/dist/lib/generators/asset-generator/guide.js.map +1 -0
  50. package/dist/lib/generators/asset-generator/html-patcher.js +65 -0
  51. package/dist/lib/generators/asset-generator/html-patcher.js.map +1 -0
  52. package/dist/lib/generators/asset-generator/ico-encoder.js +29 -0
  53. package/dist/lib/generators/asset-generator/ico-encoder.js.map +1 -0
  54. package/dist/lib/generators/asset-generator/manifest-patcher.js +23 -0
  55. package/dist/lib/generators/asset-generator/manifest-patcher.js.map +1 -0
  56. package/dist/lib/generators/asset-generator/maskable.js +15 -0
  57. package/dist/lib/generators/asset-generator/maskable.js.map +1 -0
  58. package/dist/lib/generators/asset-generator/rasterize.js +32 -0
  59. package/dist/lib/generators/asset-generator/rasterize.js.map +1 -0
  60. package/dist/lib/generators/asset-generator/sizes.js +25 -0
  61. package/dist/lib/generators/asset-generator/sizes.js.map +1 -0
  62. package/dist/lib/generators/file-generators/api-config.js +6 -0
  63. package/dist/lib/generators/file-generators/api-config.js.map +1 -0
  64. package/dist/lib/generators/file-generators/auth-state.js +2 -34
  65. package/dist/lib/generators/file-generators/auth-state.js.map +1 -1
  66. package/dist/lib/generators/file-generators/auth-store.js +2 -250
  67. package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
  68. package/dist/lib/generators/file-generators/auth-user.js +3 -81
  69. package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
  70. package/dist/lib/generators/file-generators/background-sync.js +2 -57
  71. package/dist/lib/generators/file-generators/background-sync.js.map +1 -1
  72. package/dist/lib/generators/file-generators/cache.js +2 -38
  73. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  74. package/dist/lib/generators/file-generators/client-injector.js +2 -91
  75. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  76. package/dist/lib/generators/file-generators/fetch-state.js +6 -0
  77. package/dist/lib/generators/file-generators/fetch-state.js.map +1 -0
  78. package/dist/lib/generators/file-generators/fetch-wrapper.js +3 -267
  79. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  80. package/dist/lib/generators/file-generators/generate-framework-adapters.js +56 -0
  81. package/dist/lib/generators/file-generators/generate-framework-adapters.js.map +1 -0
  82. package/dist/lib/generators/file-generators/generate-hooks.js +4 -3
  83. package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
  84. package/dist/lib/generators/file-generators/gql-wrapper.js +3 -145
  85. package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -1
  86. package/dist/lib/generators/file-generators/guide-generator.js +24 -630
  87. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
  88. package/dist/lib/generators/file-generators/invalidation-tags.js +18 -86
  89. package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
  90. package/dist/lib/generators/file-generators/manifest.js +15 -5
  91. package/dist/lib/generators/file-generators/manifest.js.map +1 -1
  92. package/dist/lib/generators/file-generators/mutation-queue.js +3 -285
  93. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  94. package/dist/lib/generators/file-generators/mutation-state.js +2 -117
  95. package/dist/lib/generators/file-generators/mutation-state.js.map +1 -1
  96. package/dist/lib/generators/file-generators/push.js +6 -139
  97. package/dist/lib/generators/file-generators/push.js.map +1 -1
  98. package/dist/lib/generators/file-generators/pwa-install.js +11 -84
  99. package/dist/lib/generators/file-generators/pwa-install.js.map +1 -1
  100. package/dist/lib/generators/file-generators/quick-readme.js +8 -125
  101. package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
  102. package/dist/lib/generators/file-generators/reset.js +6 -0
  103. package/dist/lib/generators/file-generators/reset.js.map +1 -0
  104. package/dist/lib/generators/file-generators/server-push.js +3 -124
  105. package/dist/lib/generators/file-generators/server-push.js.map +1 -1
  106. package/dist/lib/generators/file-generators/sw-generator-build.js +8 -9
  107. package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
  108. package/dist/lib/generators/file-generators/sw-injector.js +10 -230
  109. package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
  110. package/dist/lib/generators/file-generators/type-definitions.js +2 -114
  111. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
  112. package/dist/lib/generators/sw-generator.js +10 -20
  113. package/dist/lib/generators/sw-generator.js.map +1 -1
  114. package/dist/lib/generators/sw-sections/activate-handler.js +25 -3
  115. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
  116. package/dist/lib/generators/sw-sections/assemble-sw.js +20 -16
  117. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  118. package/dist/lib/generators/sw-sections/background-sync-handler.js +68 -19
  119. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
  120. package/dist/lib/generators/sw-sections/config-header.js +2 -2
  121. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  122. package/dist/lib/generators/sw-sections/fetch-handler.js +506 -144
  123. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  124. package/dist/lib/generators/sw-sections/glob-matcher-code.js.map +1 -0
  125. package/dist/lib/generators/sw-sections/message-handler.js +55 -15
  126. package/dist/lib/generators/sw-sections/message-handler.js.map +1 -1
  127. package/dist/lib/generators/sw-sections/server-push-handler.js +12 -0
  128. package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -1
  129. package/dist/lib/generators/sw-sections/sw-push.js +67 -0
  130. package/dist/lib/generators/sw-sections/sw-push.js.map +1 -0
  131. package/dist/lib/generators/sw-sections/tag-management.js +65 -23
  132. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  133. package/dist/lib/generators/swoff-files-generator.js +66 -45
  134. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  135. package/dist/lib/shared/config-types.js +182 -71
  136. package/dist/lib/shared/config-types.js.map +1 -1
  137. package/dist/lib/shared/glob-matcher.js +77 -0
  138. package/dist/lib/shared/glob-matcher.js.map +1 -0
  139. package/dist/lib/utils/tty-status.js +21 -0
  140. package/dist/lib/utils/tty-status.js.map +1 -0
  141. package/dist/runtime/api-config.js +18 -0
  142. package/dist/runtime/api-config.js.map +1 -0
  143. package/dist/runtime/auth-state.js +35 -0
  144. package/dist/runtime/auth-state.js.map +1 -0
  145. package/dist/runtime/auth-store.js +273 -0
  146. package/dist/runtime/auth-store.js.map +1 -0
  147. package/dist/runtime/auth-user.js +168 -0
  148. package/dist/runtime/auth-user.js.map +1 -0
  149. package/dist/runtime/background-sync.js +59 -0
  150. package/dist/runtime/background-sync.js.map +1 -0
  151. package/dist/runtime/cache.js +30 -0
  152. package/dist/runtime/cache.js.map +1 -0
  153. package/dist/runtime/client-injector.js +136 -0
  154. package/dist/runtime/client-injector.js.map +1 -0
  155. package/dist/runtime/fetch-state.js +42 -0
  156. package/dist/runtime/fetch-state.js.map +1 -0
  157. package/dist/runtime/fetch-wrapper.js +355 -0
  158. package/dist/runtime/fetch-wrapper.js.map +1 -0
  159. package/dist/runtime/gql-wrapper.js +135 -0
  160. package/dist/runtime/gql-wrapper.js.map +1 -0
  161. package/dist/runtime/invalidation-tags.js +260 -0
  162. package/dist/runtime/invalidation-tags.js.map +1 -0
  163. package/dist/runtime/mutation-queue.js +304 -0
  164. package/dist/runtime/mutation-queue.js.map +1 -0
  165. package/dist/runtime/mutation-state.js +117 -0
  166. package/dist/runtime/mutation-state.js.map +1 -0
  167. package/dist/runtime/push.js +138 -0
  168. package/dist/runtime/push.js.map +1 -0
  169. package/dist/runtime/pwa-index.js +11 -0
  170. package/dist/runtime/pwa-index.js.map +1 -0
  171. package/dist/runtime/pwa-injector.js +44 -0
  172. package/dist/runtime/pwa-injector.js.map +1 -0
  173. package/dist/runtime/pwa-install.js +83 -0
  174. package/dist/runtime/pwa-install.js.map +1 -0
  175. package/dist/runtime/pwa-prompt.js +54 -0
  176. package/dist/runtime/pwa-prompt.js.map +1 -0
  177. package/dist/runtime/reset.js +148 -0
  178. package/dist/runtime/reset.js.map +1 -0
  179. package/dist/runtime/server-push.js +148 -0
  180. package/dist/runtime/server-push.js.map +1 -0
  181. package/dist/runtime/sw-injector.js +236 -0
  182. package/dist/runtime/sw-injector.js.map +1 -0
  183. package/dist/runtime/type-definitions.js +138 -0
  184. package/dist/runtime/type-definitions.js.map +1 -0
  185. package/dist/runtime/utils.js +27 -0
  186. package/dist/runtime/utils.js.map +1 -0
  187. package/package.json +3 -1
  188. package/templates/react/useAuth.jsx +106 -0
  189. package/templates/react/useAuth.tsx +118 -0
  190. package/templates/{hooks → react}/useBackgroundSync.jsx +6 -4
  191. package/templates/{hooks → react}/useBackgroundSync.tsx +6 -4
  192. package/templates/{hooks → react}/useCacheInvalidation.jsx +2 -2
  193. package/templates/react/useCacheInvalidation.tsx +52 -0
  194. package/templates/react/useCachedFetch.jsx +140 -0
  195. package/templates/react/useCachedFetch.tsx +160 -0
  196. package/templates/react/useIsFetching.jsx +32 -0
  197. package/templates/react/useIsFetching.tsx +32 -0
  198. package/templates/react/useMutation.jsx +141 -0
  199. package/templates/react/useMutation.tsx +180 -0
  200. package/templates/react/useMutationQueue.jsx +69 -0
  201. package/templates/react/useMutationQueue.tsx +83 -0
  202. package/templates/react/useMutationState.jsx +13 -0
  203. package/templates/react/useMutationState.tsx +25 -0
  204. package/templates/react/useNetworkStatus.jsx +61 -0
  205. package/templates/react/useNetworkStatus.tsx +76 -0
  206. package/templates/react/usePrefetch.jsx +32 -0
  207. package/templates/react/usePrefetch.tsx +42 -0
  208. package/templates/{hooks → react}/usePushSubscription.jsx +7 -7
  209. package/templates/{hooks → react}/usePushSubscription.tsx +7 -7
  210. package/templates/{hooks → react}/useSWUpdate.jsx +3 -3
  211. package/templates/{hooks → react}/useSWUpdate.tsx +4 -4
  212. package/templates/react/useSwoffReset.jsx +32 -0
  213. package/templates/react/useSwoffReset.tsx +39 -0
  214. package/templates/hooks/useAuth.jsx +0 -30
  215. package/templates/hooks/useAuth.tsx +0 -30
  216. package/templates/hooks/useCacheInvalidation.tsx +0 -19
  217. package/templates/hooks/useCachedFetch.jsx +0 -94
  218. package/templates/hooks/useCachedFetch.tsx +0 -112
  219. package/templates/hooks/useMutation.jsx +0 -76
  220. package/templates/hooks/useMutation.tsx +0 -97
  221. package/templates/hooks/useMutationQueue.jsx +0 -42
  222. package/templates/hooks/useMutationQueue.tsx +0 -49
  223. package/templates/hooks/useMutationState.jsx +0 -27
  224. package/templates/hooks/useMutationState.tsx +0 -41
  225. package/templates/hooks/useNetworkStatus.jsx +0 -19
  226. package/templates/hooks/useNetworkStatus.tsx +0 -19
  227. package/templates/hooks/usePrefetch.jsx +0 -10
  228. package/templates/hooks/usePrefetch.tsx +0 -25
@@ -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 "../background-sync.js";
2
+ import { retrySync } from "../offline/sync.js";
3
3
 
4
4
  export function useBackgroundSync() {
5
- const [state, setState] = useState({
6
- supported: "serviceWorker" in navigator && "SyncManager" in window,
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 "../background-sync.ts";
2
+ import { retrySync } from "../offline/sync.ts";
3
3
 
4
4
  export function useBackgroundSync() {
5
- const [state, setState] = useState({
6
- supported: "serviceWorker" in navigator && "SyncManager" in window,
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 "../invalidation-tags.js";
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
+ }