@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,141 @@
1
+ import { useState, useCallback, useRef } from "react";
2
+ import { fetchWithCache } from "../fetch/core.js";
3
+ import {
4
+ trackMutation,
5
+ resolveMutation,
6
+ rejectMutation,
7
+ } from "../offline/state.js";
8
+
9
+ /**
10
+ * Hook for mutations (POST, PUT, PATCH, DELETE) with auto-invalidation, offline queuing,
11
+ * optimistic updates, and dual-level callbacks.
12
+ *
13
+ * Usage:
14
+ * const { mutate, isLoading, error, data } = useMutation("/api/todos", {
15
+ * method: "POST",
16
+ * onSuccess: (data) => navigate(`/item/${data.id}`),
17
+ * });
18
+ *
19
+ * // Called multiple times — all share one loading/error state:
20
+ * await mutate({ title: "hello" });
21
+ * await mutate({ title: "world" }, {
22
+ * onSuccess: (data) => console.log(data),
23
+ * });
24
+ *
25
+ * // Optimistic update with rollback:
26
+ * const { mutate } = useMutation("/api/todos", {
27
+ * method: "POST",
28
+ * onMutate: () => { /* set optimistic state */ },
29
+ * onError: () => { /* rollback */ },
30
+ * });
31
+ */
32
+ export function useMutation(url, options = {}) {
33
+ const [state, setState] = useState({
34
+ data: null,
35
+ error: null,
36
+ isLoading: false,
37
+ isError: false,
38
+ isSuccess: false,
39
+ });
40
+
41
+ const [mutationId, setMutationId] = useState(null);
42
+ const optionsRef = useRef(options);
43
+ const urlRef = useRef(url);
44
+ const inFlightKeys = useRef(new Set());
45
+
46
+ urlRef.current = url;
47
+
48
+ const mutate = useCallback(async (body, callbacks) => {
49
+ const key = optionsRef.current.mutationKey;
50
+ if (key && inFlightKeys.current.has(key)) return null;
51
+ if (key) inFlightKeys.current.add(key);
52
+
53
+ let retriesLeft = optionsRef.current.retry === true
54
+ ? Infinity
55
+ : (optionsRef.current.retry ?? 0);
56
+
57
+ const mid = "mut-" + crypto.randomUUID();
58
+ setMutationId(mid);
59
+ trackMutation(mid, "pending");
60
+ setState({
61
+ data: null,
62
+ error: null,
63
+ isLoading: true,
64
+ isError: false,
65
+ isSuccess: false,
66
+ });
67
+
68
+ optionsRef.current.onMutate?.();
69
+
70
+ const attempt = async () => {
71
+ try {
72
+ const fetchOptions = { ...optionsRef.current, body };
73
+ const { response, queued } = await fetchWithCache(urlRef.current, fetchOptions);
74
+ if (queued) {
75
+ resolveMutation(mid, null);
76
+ setState({
77
+ data: null,
78
+ error: null,
79
+ isLoading: false,
80
+ isError: false,
81
+ isSuccess: false,
82
+ });
83
+ optionsRef.current.onSettled?.();
84
+ callbacks?.onSettled?.();
85
+ if (key) inFlightKeys.current.delete(key);
86
+ return null;
87
+ }
88
+ const data = await response.json();
89
+ resolveMutation(mid, data);
90
+ setState({
91
+ data,
92
+ error: null,
93
+ isLoading: false,
94
+ isError: false,
95
+ isSuccess: true,
96
+ });
97
+ optionsRef.current.onSuccess?.(data);
98
+ callbacks?.onSuccess?.(data);
99
+ optionsRef.current.onSettled?.();
100
+ callbacks?.onSettled?.();
101
+ if (key) inFlightKeys.current.delete(key);
102
+ return data;
103
+ } catch (err) {
104
+ if (retriesLeft > 0) {
105
+ retriesLeft--;
106
+ await new Promise((r) => setTimeout(r, 1000));
107
+ return attempt();
108
+ }
109
+ const error = err instanceof Error ? err : new Error(String(err));
110
+ rejectMutation(mid, error);
111
+ setState({
112
+ data: null,
113
+ error,
114
+ isLoading: false,
115
+ isError: true,
116
+ isSuccess: false,
117
+ });
118
+ optionsRef.current.onError?.(error);
119
+ callbacks?.onError?.(error);
120
+ optionsRef.current.onSettled?.();
121
+ callbacks?.onSettled?.();
122
+ if (key) inFlightKeys.current.delete(key);
123
+ return null;
124
+ }
125
+ };
126
+
127
+ return attempt();
128
+ }, []);
129
+
130
+ const reset = useCallback(() => {
131
+ setState({
132
+ data: null,
133
+ error: null,
134
+ isLoading: false,
135
+ isError: false,
136
+ isSuccess: false,
137
+ });
138
+ }, []);
139
+
140
+ return { ...state, mutate, reset, mutationId };
141
+ }
@@ -0,0 +1,180 @@
1
+ import { useState, useCallback, useRef } from "react";
2
+ import { fetchWithCache } from "../fetch/core.ts";
3
+ import {
4
+ trackMutation,
5
+ resolveMutation,
6
+ rejectMutation,
7
+ } from "../offline/state.ts";
8
+ import type { FetchWithCacheOptions } from "../fetch/core.ts";
9
+
10
+ export interface MutateCallbacks<TData> {
11
+ onSuccess?: (data: TData) => void;
12
+ onError?: (error: Error) => void;
13
+ onSettled?: () => void;
14
+ }
15
+
16
+ export interface UseMutationOptions<TData> extends FetchWithCacheOptions {
17
+ /** Called before the mutation fires. Use for optimistic UI — set local state here, then rollback from onError. */
18
+ onMutate?: () => void;
19
+ onSuccess?: (data: TData) => void;
20
+ onError?: (error: Error) => void;
21
+ onSettled?: () => void;
22
+ /** Deduplication key — if a mutation with the same key is already in-flight, this one is skipped. */
23
+ mutationKey?: string;
24
+ /** Retry count on failure (default 0). true = Infinity. */
25
+ retry?: number | boolean;
26
+ }
27
+
28
+ /**
29
+ * Hook for mutations (POST, PUT, PATCH, DELETE) with auto-invalidation, offline queuing,
30
+ * optimistic updates, and dual-level callbacks.
31
+ *
32
+ * Usage:
33
+ * const { mutate, isLoading, error, data } = useMutation<CreateTodoResponse>(
34
+ * "/api/todos",
35
+ * { method: "POST", onSuccess: (data) => navigate(`/item/${data.id}`) },
36
+ * );
37
+ *
38
+ * // Called multiple times — all share one loading/error state:
39
+ * await mutate({ title: "hello" });
40
+ * await mutate({ title: "world" }, {
41
+ * onSuccess: (data) => console.log(data),
42
+ * });
43
+ *
44
+ * // Optimistic update with rollback:
45
+ * const { mutate } = useMutation("/api/todos", {
46
+ * method: "POST",
47
+ * onMutate: () => { /* set optimistic state *\/ },
48
+ * onError: () => { /* rollback *\/ },
49
+ * });
50
+ */
51
+ export function useMutation<TData = unknown>(
52
+ url: string,
53
+ options: UseMutationOptions<TData> = {},
54
+ ) {
55
+ const [state, setState] = useState<{
56
+ data: TData | null;
57
+ error: Error | null;
58
+ isLoading: boolean;
59
+ isError: boolean;
60
+ isSuccess: boolean;
61
+ }>({
62
+ data: null,
63
+ error: null,
64
+ isLoading: false,
65
+ isError: false,
66
+ isSuccess: false,
67
+ });
68
+
69
+ const [mutationId, setMutationId] = useState<string | null>(null);
70
+ const optionsRef = useRef(options);
71
+ const urlRef = useRef(url);
72
+ const inFlightKeys = useRef(new Set<string>());
73
+
74
+ urlRef.current = url;
75
+
76
+ const mutate = useCallback(
77
+ async (
78
+ body?: unknown,
79
+ callbacks?: MutateCallbacks<TData>,
80
+ ): Promise<TData | null> => {
81
+ const key = optionsRef.current.mutationKey;
82
+ if (key && inFlightKeys.current.has(key)) return null;
83
+ if (key) inFlightKeys.current.add(key);
84
+
85
+ let retriesLeft = optionsRef.current.retry === true
86
+ ? Infinity
87
+ : (optionsRef.current.retry ?? 0);
88
+
89
+ const mid = "mut-" + crypto.randomUUID();
90
+ setMutationId(mid);
91
+ trackMutation(mid, "pending");
92
+ setState({
93
+ data: null,
94
+ error: null,
95
+ isLoading: true,
96
+ isError: false,
97
+ isSuccess: false,
98
+ });
99
+
100
+ optionsRef.current.onMutate?.();
101
+
102
+ const attempt = async (): Promise<TData | null> => {
103
+ try {
104
+ const fetchOptions = { ...optionsRef.current, body };
105
+ const { response, queued } = await fetchWithCache<TData>(urlRef.current, fetchOptions);
106
+ if (queued) {
107
+ resolveMutation(mid, null);
108
+ setState({
109
+ data: null,
110
+ error: null,
111
+ isLoading: false,
112
+ isError: false,
113
+ isSuccess: false,
114
+ });
115
+ optionsRef.current.onSettled?.();
116
+ callbacks?.onSettled?.();
117
+ if (key) inFlightKeys.current.delete(key);
118
+ return null;
119
+ }
120
+ const data: TData = await response.json();
121
+ resolveMutation(mid, data);
122
+ setState({
123
+ data,
124
+ error: null,
125
+ isLoading: false,
126
+ isError: false,
127
+ isSuccess: true,
128
+ });
129
+ optionsRef.current.onSuccess?.(data);
130
+ callbacks?.onSuccess?.(data);
131
+ optionsRef.current.onSettled?.();
132
+ callbacks?.onSettled?.();
133
+ if (key) inFlightKeys.current.delete(key);
134
+ return data;
135
+ } catch (err) {
136
+ if (retriesLeft > 0) {
137
+ retriesLeft--;
138
+ await new Promise((r) => setTimeout(r, 1000));
139
+ return attempt();
140
+ }
141
+ const error = err instanceof Error ? err : new Error(String(err));
142
+ rejectMutation(mid, error);
143
+ setState({
144
+ data: null,
145
+ error,
146
+ isLoading: false,
147
+ isError: true,
148
+ isSuccess: false,
149
+ });
150
+ optionsRef.current.onError?.(error);
151
+ callbacks?.onError?.(error);
152
+ optionsRef.current.onSettled?.();
153
+ callbacks?.onSettled?.();
154
+ if (key) inFlightKeys.current.delete(key);
155
+ return null;
156
+ }
157
+ };
158
+
159
+ return attempt();
160
+ },
161
+ [],
162
+ );
163
+
164
+ const reset = useCallback(() => {
165
+ setState({
166
+ data: null,
167
+ error: null,
168
+ isLoading: false,
169
+ isError: false,
170
+ isSuccess: false,
171
+ });
172
+ }, []);
173
+
174
+ return {
175
+ ...state,
176
+ mutate,
177
+ reset,
178
+ mutationId,
179
+ };
180
+ }
@@ -0,0 +1,69 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { getPendingCount, getQueueItems, processMutationQueue } from "../offline/queue.js";
3
+
4
+ /**
5
+ * Reactive mutation queue state: pending count, items, last sync info, and processing flag.
6
+ *
7
+ * Usage:
8
+ * const { pending, items, lastSync, isProcessing, retryAll } = useMutationQueue();
9
+ *
10
+ * // Show pending badge:
11
+ * {pending > 0 && <Badge>{pending}</Badge>}
12
+ *
13
+ * // Retry all queued mutations:
14
+ * <button onClick={retryAll} disabled={isProcessing}>
15
+ * {isProcessing ? "Syncing..." : "Retry All"}
16
+ * </button>
17
+ */
18
+ export function useMutationQueue() {
19
+ const [state, setState] = useState({
20
+ pending: 0,
21
+ items: [],
22
+ lastSync: null,
23
+ isProcessing: false,
24
+ });
25
+
26
+ const refresh = useCallback(async () => {
27
+ const [count, items] = await Promise.all([
28
+ getPendingCount(),
29
+ getQueueItems(),
30
+ ]);
31
+ setState((s) => ({ ...s, pending: count, items }));
32
+ }, []);
33
+
34
+ useEffect(() => {
35
+ queueMicrotask(() => refresh());
36
+
37
+ const onSync = (e) => {
38
+ setState((s) => ({
39
+ ...s,
40
+ isProcessing: false,
41
+ lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
42
+ }));
43
+ refresh();
44
+ };
45
+ const onChange = () => refresh();
46
+ const onProgress = () => setState((s) => ({ ...s, isProcessing: true }));
47
+
48
+ window.addEventListener("mutation-sync-complete", onSync);
49
+ window.addEventListener("mutation-queue-changed", onChange);
50
+ window.addEventListener("mutation-sync-progress", onProgress);
51
+ return () => {
52
+ window.removeEventListener("mutation-sync-complete", onSync);
53
+ window.removeEventListener("mutation-queue-changed", onChange);
54
+ window.removeEventListener("mutation-sync-progress", onProgress);
55
+ };
56
+ }, [refresh]);
57
+
58
+ const retryAll = useCallback(async () => {
59
+ setState((s) => ({ ...s, isProcessing: true }));
60
+ try {
61
+ await processMutationQueue();
62
+ } finally {
63
+ await refresh();
64
+ setState((s) => ({ ...s, isProcessing: false }));
65
+ }
66
+ }, [refresh]);
67
+
68
+ return { ...state, retryAll };
69
+ }
@@ -0,0 +1,83 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { getPendingCount, getQueueItems, processMutationQueue } from "../offline/queue.ts";
3
+ import type { MutationQueueItem } from "../swoff.d.ts";
4
+
5
+ export interface MutationQueueState {
6
+ pending: number;
7
+ items: MutationQueueItem[];
8
+ lastSync: { succeeded: number; failed: number } | null;
9
+ isProcessing: boolean;
10
+ }
11
+
12
+ /**
13
+ * Reactive mutation queue state: pending count, items, last sync info, and processing flag.
14
+ *
15
+ * Usage:
16
+ * const { pending, items, lastSync, isProcessing, retryAll } = useMutationQueue();
17
+ *
18
+ * // Show pending badge:
19
+ * {pending > 0 && <Badge>{pending}</Badge>}
20
+ *
21
+ * // Retry all queued mutations:
22
+ * <button onClick={retryAll} disabled={isProcessing}>
23
+ * {isProcessing ? "Syncing..." : "Retry All"}
24
+ * </button>
25
+ *
26
+ * @returns {{ pending, items, lastSync, isProcessing, retryAll }}
27
+ */
28
+ export function useMutationQueue(): MutationQueueState & { retryAll: () => Promise<void> } {
29
+ const [state, setState] = useState<MutationQueueState>({
30
+ pending: 0,
31
+ items: [],
32
+ lastSync: null,
33
+ isProcessing: false,
34
+ });
35
+
36
+ const refresh = useCallback(async () => {
37
+ const [count, items] = await Promise.all([
38
+ getPendingCount(),
39
+ getQueueItems(),
40
+ ]);
41
+ setState((s) => ({ ...s, pending: count, items }));
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ queueMicrotask(() => refresh());
46
+
47
+ const onSync = (e: CustomEvent) => {
48
+ setState((s) => ({
49
+ ...s,
50
+ isProcessing: false,
51
+ lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
52
+ }));
53
+ refresh();
54
+ };
55
+ const onChange = () => {
56
+ refresh();
57
+ };
58
+ const onProgress = () => {
59
+ setState((s) => ({ ...s, isProcessing: true }));
60
+ };
61
+
62
+ window.addEventListener("mutation-sync-complete", onSync);
63
+ window.addEventListener("mutation-queue-changed", onChange);
64
+ window.addEventListener("mutation-sync-progress", onProgress);
65
+ return () => {
66
+ window.removeEventListener("mutation-sync-complete", onSync);
67
+ window.removeEventListener("mutation-queue-changed", onChange);
68
+ window.removeEventListener("mutation-sync-progress", onProgress);
69
+ };
70
+ }, [refresh]);
71
+
72
+ const retryAll = useCallback(async () => {
73
+ setState((s) => ({ ...s, isProcessing: true }));
74
+ try {
75
+ await processMutationQueue();
76
+ } finally {
77
+ await refresh();
78
+ setState((s) => ({ ...s, isProcessing: false }));
79
+ }
80
+ }, [refresh]);
81
+
82
+ return { ...state, retryAll };
83
+ }
@@ -0,0 +1,13 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { getMutationState, onMutationStateChange } from "../mutation-state.js";
3
+
4
+ function subscribeToMutations(cb) {
5
+ return onMutationStateChange(() => cb());
6
+ }
7
+
8
+ export function useMutationState(id) {
9
+ return useSyncExternalStore(
10
+ subscribeToMutations,
11
+ () => (id ? getMutationState(id) ?? null : null),
12
+ );
13
+ }
@@ -0,0 +1,25 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import {
3
+ getMutationState,
4
+ onMutationStateChange,
5
+ } from "../mutation-state.ts";
6
+ import type { MutationState } from "../mutation-state.ts";
7
+
8
+ function subscribeToMutations(cb: () => void) {
9
+ return onMutationStateChange(() => cb());
10
+ }
11
+
12
+ /**
13
+ * Hook that subscribes to a specific mutation's state changes.
14
+ * Useful for showing per-mutation loading spinners, error states, etc.
15
+ *
16
+ * Usage:
17
+ * const mutation = useMutationState(mutationId);
18
+ * if (mutation?.status === "error") { ... }
19
+ */
20
+ export function useMutationState(id: string | null): MutationState | null {
21
+ return useSyncExternalStore(
22
+ subscribeToMutations,
23
+ () => (id ? getMutationState(id) ?? null : null),
24
+ );
25
+ }
@@ -0,0 +1,61 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * Reactive network information: online status, connection type, and bandwidth.
5
+ *
6
+ * Usage:
7
+ * const { online, wasOffline, lastChangedAt, effectiveType, downlink } = useNetworkStatus();
8
+ *
9
+ * // Show offline indicator:
10
+ * if (!online) return <OfflineBanner />;
11
+ *
12
+ * // Warn on slow connection:
13
+ * if (online && effectiveType === "2g") return <SlowConnectionWarning />;
14
+ */
15
+ export function useNetworkStatus() {
16
+ const wasOfflineRef = useRef(false);
17
+
18
+ const [state, setState] = useState(() => {
19
+ const online = typeof navigator !== "undefined" ? navigator.onLine : true;
20
+ const connection = navigator?.connection;
21
+ return {
22
+ online,
23
+ wasOffline: false,
24
+ lastChangedAt: null,
25
+ effectiveType: connection?.effectiveType ?? null,
26
+ downlink: connection?.downlink ?? null,
27
+ };
28
+ });
29
+
30
+ useEffect(() => {
31
+ const onOnline = () => {
32
+ setState((s) => ({ ...s, online: true, lastChangedAt: Date.now() }));
33
+ };
34
+ const onOffline = () => {
35
+ wasOfflineRef.current = true;
36
+ setState((s) => ({ ...s, online: false, lastChangedAt: Date.now(), wasOffline: true }));
37
+ };
38
+
39
+ const connection = navigator?.connection;
40
+ const onTypeChange = () => {
41
+ if (!connection) return;
42
+ setState((s) => ({ ...s, effectiveType: connection.effectiveType, downlink: connection.downlink }));
43
+ };
44
+
45
+ window.addEventListener("online", onOnline);
46
+ window.addEventListener("offline", onOffline);
47
+ if (connection) {
48
+ connection.addEventListener("change", onTypeChange);
49
+ }
50
+
51
+ return () => {
52
+ window.removeEventListener("online", onOnline);
53
+ window.removeEventListener("offline", onOffline);
54
+ if (connection) {
55
+ connection.removeEventListener("change", onTypeChange);
56
+ }
57
+ };
58
+ }, []);
59
+
60
+ return state;
61
+ }
@@ -0,0 +1,76 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * Reactive network information: online status, connection type, and bandwidth.
5
+ *
6
+ * Usage:
7
+ * const { online, wasOffline, lastChangedAt, effectiveType, downlink } = useNetworkStatus();
8
+ *
9
+ * // Show offline indicator:
10
+ * if (!online) return <OfflineBanner />;
11
+ *
12
+ * // Warn on slow connection:
13
+ * if (online && effectiveType === "2g") return <SlowConnectionWarning />;
14
+ *
15
+ * @returns {{ online: boolean, wasOffline: boolean, lastChangedAt: number | null, effectiveType: string | null, downlink: number | null }}
16
+ */
17
+ export function useNetworkStatus() {
18
+ const wasOfflineRef = useRef(false);
19
+
20
+ const [state, setState] = useState(() => {
21
+ const online = typeof navigator !== "undefined" ? navigator.onLine : true;
22
+ const connection = navigator?.connection;
23
+ return {
24
+ online,
25
+ wasOffline: false,
26
+ lastChangedAt: null as number | null,
27
+ effectiveType: connection?.effectiveType ?? null,
28
+ downlink: connection?.downlink ?? null,
29
+ };
30
+ });
31
+
32
+ useEffect(() => {
33
+ const onOnline = () => {
34
+ setState((s) => ({
35
+ ...s,
36
+ online: true,
37
+ lastChangedAt: Date.now(),
38
+ }));
39
+ };
40
+ const onOffline = () => {
41
+ wasOfflineRef.current = true;
42
+ setState((s) => ({
43
+ ...s,
44
+ online: false,
45
+ lastChangedAt: Date.now(),
46
+ wasOffline: true,
47
+ }));
48
+ };
49
+
50
+ const connection = navigator?.connection;
51
+ const onTypeChange = () => {
52
+ if (!connection) return;
53
+ setState((s) => ({
54
+ ...s,
55
+ effectiveType: connection.effectiveType,
56
+ downlink: connection.downlink,
57
+ }));
58
+ };
59
+
60
+ window.addEventListener("online", onOnline);
61
+ window.addEventListener("offline", onOffline);
62
+ if (connection) {
63
+ connection.addEventListener("change", onTypeChange);
64
+ }
65
+
66
+ return () => {
67
+ window.removeEventListener("online", onOnline);
68
+ window.removeEventListener("offline", onOffline);
69
+ if (connection) {
70
+ connection.removeEventListener("change", onTypeChange);
71
+ }
72
+ };
73
+ }, []);
74
+
75
+ return state;
76
+ }
@@ -0,0 +1,32 @@
1
+ import { useCallback, useState } from "react";
2
+ import { prefetchCache } from "../fetch/core.js";
3
+
4
+ /**
5
+ * Hook that returns a stable prefetch callback, observable prefetch list, and clear function.
6
+ *
7
+ * Usage:
8
+ * const { prefetch, prefetchList, clear } = usePrefetch();
9
+ *
10
+ * // Prefetch when user hovers a link:
11
+ * <Link onMouseEnter={() => prefetch("/api/todos")} to="/todos" />
12
+ *
13
+ * // Show how many URLs are prefetched:
14
+ * <span>{prefetchList.length} routes cached</span>
15
+ *
16
+ * // Clear prefetched data:
17
+ * <button onClick={clear}>Clear prefetch</button>
18
+ */
19
+ export function usePrefetch() {
20
+ const [prefetchList, setPrefetchList] = useState([]);
21
+
22
+ const prefetch = useCallback((url, options) => {
23
+ prefetchCache(url, options);
24
+ setPrefetchList((list) => list.includes(url) ? list : [...list, url]);
25
+ }, []);
26
+
27
+ const clear = useCallback(() => {
28
+ setPrefetchList([]);
29
+ }, []);
30
+
31
+ return { prefetch, prefetchList, clear };
32
+ }