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