@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,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,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
+ }
@@ -0,0 +1,42 @@
1
+ import { useCallback, useState } from "react";
2
+ import { prefetchCache } from "../fetch/core.ts";
3
+ import type { FetchWithCacheOptions } from "../fetch/core.ts";
4
+
5
+ /**
6
+ * Hook that returns a stable prefetch callback, observable prefetch list, and clear function.
7
+ *
8
+ * Usage:
9
+ * const { prefetch, prefetchList, clear } = usePrefetch();
10
+ *
11
+ * // Prefetch when user hovers a link:
12
+ * <Link onMouseEnter={() => prefetch("/api/todos")} to="/todos" />
13
+ *
14
+ * // Show how many URLs are prefetched:
15
+ * <span>{prefetchList.length} routes cached</span>
16
+ *
17
+ * // Clear prefetched data:
18
+ * <button onClick={clear}>Clear prefetch</button>
19
+ *
20
+ * @returns {{ prefetch: (url: string, options?: FetchWithCacheOptions) => void, prefetchList: string[], clear: () => void }}
21
+ */
22
+ export function usePrefetch(): {
23
+ prefetch: (url: string, options?: FetchWithCacheOptions) => void;
24
+ prefetchList: string[];
25
+ clear: () => void;
26
+ } {
27
+ const [prefetchList, setPrefetchList] = useState<string[]>([]);
28
+
29
+ const prefetch = useCallback(
30
+ (url: string, options?: FetchWithCacheOptions) => {
31
+ prefetchCache(url, options);
32
+ setPrefetchList((list) => list.includes(url) ? list : [...list, url]);
33
+ },
34
+ [],
35
+ );
36
+
37
+ const clear = useCallback(() => {
38
+ setPrefetchList([]);
39
+ }, []);
40
+
41
+ return { prefetch, prefetchList, clear };
42
+ }