@swoff/cli 0.3.13 → 0.3.14

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 (224) hide show
  1. package/README.md +28 -32
  2. package/dist/index.js +2 -13
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/cli/help.js +3 -8
  5. package/dist/lib/cli/help.js.map +1 -1
  6. package/dist/lib/commands/add.js +29 -87
  7. package/dist/lib/commands/add.js.map +1 -1
  8. package/dist/lib/commands/generate-guide.js +16 -30
  9. package/dist/lib/commands/generate-guide.js.map +1 -1
  10. package/dist/lib/commands/generate.js +31 -6
  11. package/dist/lib/commands/generate.js.map +1 -1
  12. package/dist/lib/commands/init.js +188 -11
  13. package/dist/lib/commands/init.js.map +1 -1
  14. package/dist/lib/commands/validate.js +2 -4
  15. package/dist/lib/commands/validate.js.map +1 -1
  16. package/dist/lib/config/loader.js +34 -25
  17. package/dist/lib/config/loader.js.map +1 -1
  18. package/dist/lib/config/validator.js +202 -96
  19. package/dist/lib/config/validator.js.map +1 -1
  20. package/dist/lib/generators/file-generators/api-config.js +1 -1
  21. package/dist/lib/generators/file-generators/api-config.js.map +1 -1
  22. package/dist/lib/generators/file-generators/auth-adapter.js +13 -0
  23. package/dist/lib/generators/file-generators/auth-adapter.js.map +1 -0
  24. package/dist/lib/generators/file-generators/auth-check.js +40 -0
  25. package/dist/lib/generators/file-generators/auth-check.js.map +1 -0
  26. package/dist/lib/generators/file-generators/auth-store.js +2 -5
  27. package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
  28. package/dist/lib/generators/file-generators/background-sync.js +1 -1
  29. package/dist/lib/generators/file-generators/background-sync.js.map +1 -1
  30. package/dist/lib/generators/file-generators/cache.js +2 -1
  31. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  32. package/dist/lib/generators/file-generators/client-injector-bundle.js +15 -0
  33. package/dist/lib/generators/file-generators/client-injector-bundle.js.map +1 -0
  34. package/dist/lib/generators/file-generators/client-injector.js +2 -1
  35. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  36. package/dist/lib/generators/file-generators/connectivity-manager.js +12 -0
  37. package/dist/lib/generators/file-generators/connectivity-manager.js.map +1 -0
  38. package/dist/lib/generators/file-generators/connectivity.js +12 -0
  39. package/dist/lib/generators/file-generators/connectivity.js.map +1 -0
  40. package/dist/lib/generators/file-generators/context.js +7 -0
  41. package/dist/lib/generators/file-generators/context.js.map +1 -1
  42. package/dist/lib/generators/file-generators/fetch-wrapper.js +1 -1
  43. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  44. package/dist/lib/generators/file-generators/generate-framework-adapters.js +29 -20
  45. package/dist/lib/generators/file-generators/generate-framework-adapters.js.map +1 -1
  46. package/dist/lib/generators/file-generators/gql-wrapper.js +2 -2
  47. package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -1
  48. package/dist/lib/generators/file-generators/guide-generator.js +51 -1
  49. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
  50. package/dist/lib/generators/file-generators/invalidation-tags.js +2 -3
  51. package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
  52. package/dist/lib/generators/file-generators/mutation-queue.js +2 -2
  53. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  54. package/dist/lib/generators/file-generators/mutation-state.js +1 -1
  55. package/dist/lib/generators/file-generators/mutation-state.js.map +1 -1
  56. package/dist/lib/generators/file-generators/notification-generator.js +6 -0
  57. package/dist/lib/generators/file-generators/notification-generator.js.map +1 -0
  58. package/dist/lib/generators/file-generators/open-db.js +6 -0
  59. package/dist/lib/generators/file-generators/open-db.js.map +1 -0
  60. package/dist/lib/generators/file-generators/push.js +1 -2
  61. package/dist/lib/generators/file-generators/push.js.map +1 -1
  62. package/dist/lib/generators/file-generators/pwa-install.js +2 -6
  63. package/dist/lib/generators/file-generators/pwa-install.js.map +1 -1
  64. package/dist/lib/generators/file-generators/server-push.js +1 -1
  65. package/dist/lib/generators/file-generators/server-push.js.map +1 -1
  66. package/dist/lib/generators/file-generators/storage-generator.js +6 -0
  67. package/dist/lib/generators/file-generators/storage-generator.js.map +1 -0
  68. package/dist/lib/generators/file-generators/storage-notify.js +6 -0
  69. package/dist/lib/generators/file-generators/storage-notify.js.map +1 -0
  70. package/dist/lib/generators/file-generators/storage.js +6 -0
  71. package/dist/lib/generators/file-generators/storage.js.map +1 -0
  72. package/dist/lib/generators/file-generators/sw-generator-build.js +96 -45
  73. package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
  74. package/dist/lib/generators/file-generators/sw-injector.js +0 -4
  75. package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
  76. package/dist/lib/generators/file-generators/sw-template.js +8 -9
  77. package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
  78. package/dist/lib/generators/file-generators/sw-version-gen.js +32 -0
  79. package/dist/lib/generators/file-generators/sw-version-gen.js.map +1 -0
  80. package/dist/lib/generators/file-generators/swoff-api-bundle.js +51 -0
  81. package/dist/lib/generators/file-generators/swoff-api-bundle.js.map +1 -0
  82. package/dist/lib/generators/sw-build-utils.js +69 -0
  83. package/dist/lib/generators/sw-build-utils.js.map +1 -0
  84. package/dist/lib/generators/sw-generator.js +23 -34
  85. package/dist/lib/generators/sw-generator.js.map +1 -1
  86. package/dist/lib/generators/sw-sections/activate-handler.js +74 -23
  87. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
  88. package/dist/lib/generators/sw-sections/assemble-sw.js +15 -94
  89. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  90. package/dist/lib/generators/sw-sections/background-sync-handler.js +74 -72
  91. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
  92. package/dist/lib/generators/sw-sections/batch-refresh-queue.js +102 -0
  93. package/dist/lib/generators/sw-sections/batch-refresh-queue.js.map +1 -0
  94. package/dist/lib/generators/sw-sections/config-header.js +2 -3
  95. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  96. package/dist/lib/generators/sw-sections/default-template.js +59 -6
  97. package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
  98. package/dist/lib/generators/sw-sections/fetch-handler.js +621 -495
  99. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  100. package/dist/lib/generators/sw-sections/install-handler.js +40 -23
  101. package/dist/lib/generators/sw-sections/install-handler.js.map +1 -1
  102. package/dist/lib/generators/sw-sections/message-handler.js +25 -8
  103. package/dist/lib/generators/sw-sections/message-handler.js.map +1 -1
  104. package/dist/lib/generators/sw-sections/server-push-handler.js +10 -14
  105. package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -1
  106. package/dist/lib/generators/sw-sections/shared.js +68 -0
  107. package/dist/lib/generators/sw-sections/shared.js.map +1 -0
  108. package/dist/lib/generators/sw-sections/sw-push.js +2 -6
  109. package/dist/lib/generators/sw-sections/sw-push.js.map +1 -1
  110. package/dist/lib/generators/sw-sections/tag-management.js +55 -86
  111. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  112. package/dist/lib/generators/swoff-files-generator.js +183 -59
  113. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  114. package/dist/lib/shared/config-types.js +67 -63
  115. package/dist/lib/shared/config-types.js.map +1 -1
  116. package/dist/lib/shared/feature-registry.js +198 -0
  117. package/dist/lib/shared/feature-registry.js.map +1 -0
  118. package/dist/lib/utils/detect-framework.js +52 -9
  119. package/dist/lib/utils/detect-framework.js.map +1 -1
  120. package/dist/runtime/api-config.js +2 -2
  121. package/dist/runtime/api-config.js.map +1 -1
  122. package/dist/runtime/auth-adapter.js +179 -0
  123. package/dist/runtime/auth-adapter.js.map +1 -0
  124. package/dist/runtime/auth-state.js +16 -6
  125. package/dist/runtime/auth-state.js.map +1 -1
  126. package/dist/runtime/auth-store.js +219 -213
  127. package/dist/runtime/auth-store.js.map +1 -1
  128. package/dist/runtime/auth-user.js +5 -19
  129. package/dist/runtime/auth-user.js.map +1 -1
  130. package/dist/runtime/background-sync.js +5 -2
  131. package/dist/runtime/background-sync.js.map +1 -1
  132. package/dist/runtime/cache.js +91 -3
  133. package/dist/runtime/cache.js.map +1 -1
  134. package/dist/runtime/client-injector-bundle.js +314 -0
  135. package/dist/runtime/client-injector-bundle.js.map +1 -0
  136. package/dist/runtime/client-injector.js +173 -30
  137. package/dist/runtime/client-injector.js.map +1 -1
  138. package/dist/runtime/connectivity-manager.js +83 -0
  139. package/dist/runtime/connectivity-manager.js.map +1 -0
  140. package/dist/runtime/connectivity.js +83 -0
  141. package/dist/runtime/connectivity.js.map +1 -0
  142. package/dist/runtime/fetch-wrapper.js +26 -59
  143. package/dist/runtime/fetch-wrapper.js.map +1 -1
  144. package/dist/runtime/gql-wrapper.js +15 -9
  145. package/dist/runtime/gql-wrapper.js.map +1 -1
  146. package/dist/runtime/invalidation-tags.js +12 -89
  147. package/dist/runtime/invalidation-tags.js.map +1 -1
  148. package/dist/runtime/mutation-queue.js +83 -31
  149. package/dist/runtime/mutation-queue.js.map +1 -1
  150. package/dist/runtime/mutation-state.js +3 -3
  151. package/dist/runtime/mutation-state.js.map +1 -1
  152. package/dist/runtime/notification.js +65 -0
  153. package/dist/runtime/notification.js.map +1 -0
  154. package/dist/runtime/open-db.js +41 -0
  155. package/dist/runtime/open-db.js.map +1 -0
  156. package/dist/runtime/push.js +15 -27
  157. package/dist/runtime/push.js.map +1 -1
  158. package/dist/runtime/pwa-injector.js +3 -32
  159. package/dist/runtime/pwa-injector.js.map +1 -1
  160. package/dist/runtime/pwa-prompt.js +35 -3
  161. package/dist/runtime/pwa-prompt.js.map +1 -1
  162. package/dist/runtime/reset.js +39 -42
  163. package/dist/runtime/reset.js.map +1 -1
  164. package/dist/runtime/server-push.js +23 -13
  165. package/dist/runtime/server-push.js.map +1 -1
  166. package/dist/runtime/storage.js +32 -0
  167. package/dist/runtime/storage.js.map +1 -0
  168. package/dist/runtime/sw-injector.js +19 -178
  169. package/dist/runtime/sw-injector.js.map +1 -1
  170. package/dist/runtime/sw-version.js +13 -0
  171. package/dist/runtime/sw-version.js.map +1 -0
  172. package/dist/runtime/swoff-api-bundle.js +1491 -0
  173. package/dist/runtime/swoff-api-bundle.js.map +1 -0
  174. package/dist/runtime/type-definitions.js +8 -7
  175. package/dist/runtime/type-definitions.js.map +1 -1
  176. package/dist/runtime/utils.js +4 -0
  177. package/dist/runtime/utils.js.map +1 -1
  178. package/dist/swoff-nkdhhmw8. +18 -0
  179. package/dist/swoff.js +5 -0
  180. package/package.json +1 -1
  181. package/templates/react/useAuth.jsx +7 -4
  182. package/templates/react/useAuth.tsx +14 -14
  183. package/templates/react/useBackgroundSync.jsx +5 -4
  184. package/templates/react/useBackgroundSync.tsx +5 -4
  185. package/templates/react/useCachedFetch.jsx +8 -0
  186. package/templates/react/useCachedFetch.tsx +8 -0
  187. package/templates/react/useMutation.jsx +60 -56
  188. package/templates/react/useMutation.tsx +65 -64
  189. package/templates/react/useMutationQueue.jsx +5 -1
  190. package/templates/react/useMutationQueue.tsx +8 -2
  191. package/templates/react/useMutationState.jsx +3 -4
  192. package/templates/react/useMutationState.tsx +4 -8
  193. package/templates/react/useNetworkStatus.jsx +57 -17
  194. package/templates/react/useNetworkStatus.tsx +42 -20
  195. package/templates/react/useOfflineAnalytics.jsx +42 -0
  196. package/templates/react/useOfflineAnalytics.tsx +50 -0
  197. package/templates/react/usePwaInstall.jsx +26 -0
  198. package/templates/react/usePwaInstall.tsx +26 -0
  199. package/templates/react/useSWUpdate.jsx +7 -52
  200. package/templates/react/useSWUpdate.tsx +12 -51
  201. package/templates/react/useStorageEstimate.jsx +56 -0
  202. package/templates/react/useStorageEstimate.tsx +67 -0
  203. package/dist/__tests__/assemble-sw.test.js +0 -145
  204. package/dist/__tests__/assemble-sw.test.js.map +0 -1
  205. package/dist/__tests__/cli-integration.test.js +0 -231
  206. package/dist/__tests__/cli-integration.test.js.map +0 -1
  207. package/dist/__tests__/config-types.test.js +0 -124
  208. package/dist/__tests__/config-types.test.js.map +0 -1
  209. package/dist/__tests__/detect-language.test.js +0 -51
  210. package/dist/__tests__/detect-language.test.js.map +0 -1
  211. package/dist/__tests__/file-generators.test.js +0 -228
  212. package/dist/__tests__/file-generators.test.js.map +0 -1
  213. package/dist/__tests__/glob-matcher.test.js +0 -86
  214. package/dist/__tests__/glob-matcher.test.js.map +0 -1
  215. package/dist/__tests__/invalidation-tags.test.js +0 -283
  216. package/dist/__tests__/invalidation-tags.test.js.map +0 -1
  217. package/dist/__tests__/loader.test.js +0 -77
  218. package/dist/__tests__/loader.test.js.map +0 -1
  219. package/dist/__tests__/validator.test.js +0 -340
  220. package/dist/__tests__/validator.test.js.map +0 -1
  221. package/templates/react/useCacheInvalidation.jsx +0 -19
  222. package/templates/react/useCacheInvalidation.tsx +0 -52
  223. package/templates/react/useIsFetching.jsx +0 -32
  224. package/templates/react/useIsFetching.tsx +0 -32
@@ -4,7 +4,7 @@ import {
4
4
  trackMutation,
5
5
  resolveMutation,
6
6
  rejectMutation,
7
- } from "../offline/state.ts";
7
+ } from "../mutation/state.ts";
8
8
  import type { FetchWithCacheOptions } from "../fetch/core.ts";
9
9
 
10
10
  export interface MutateCallbacks<TData> {
@@ -25,6 +25,12 @@ export interface UseMutationOptions<TData> extends FetchWithCacheOptions {
25
25
  retry?: number | boolean;
26
26
  }
27
27
 
28
+ export type MutateResult<T> =
29
+ | { status: "skipped" }
30
+ | { status: "queued" }
31
+ | { status: "success"; data: T }
32
+ | { status: "error"; error: Error };
33
+
28
34
  /**
29
35
  * Hook for mutations (POST, PUT, PATCH, DELETE) with auto-invalidation, offline queuing,
30
36
  * optimistic updates, and dual-level callbacks.
@@ -44,28 +50,24 @@ export interface UseMutationOptions<TData> extends FetchWithCacheOptions {
44
50
  * // Optimistic update with rollback:
45
51
  * const { mutate } = useMutation("/api/todos", {
46
52
  * method: "POST",
47
- * onMutate: () => { /* set optimistic state *\/ },
48
- * onError: () => { /* rollback *\/ },
53
+ * onMutate: () => { /* set optimistic state *\\/ },
54
+ * onError: () => { /* rollback *\\/ },
49
55
  * });
56
+ *
57
+ * // Check result status after mutate:
58
+ * const result = await mutate(body);
59
+ * if (result.status === "success") navigate(`/item/${result.data.id}`);
60
+ * if (result.status === "queued") navigate("/items");
61
+ * if (result.status === "error") setError(result.error.message);
50
62
  */
51
63
  export function useMutation<TData = unknown>(
52
64
  url: string,
53
65
  options: UseMutationOptions<TData> = {},
54
66
  ) {
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
-
67
+ const [lastResult, setLastResult] = useState<MutateResult<TData> | null>(
68
+ null,
69
+ );
70
+ const [isLoading, setIsLoading] = useState(false);
69
71
  const [mutationId, setMutationId] = useState<string | null>(null);
70
72
  const optionsRef = useRef(options);
71
73
  const urlRef = useRef(url);
@@ -77,61 +79,62 @@ export function useMutation<TData = unknown>(
77
79
  async (
78
80
  body?: unknown,
79
81
  callbacks?: MutateCallbacks<TData>,
80
- ): Promise<TData | null> => {
82
+ ): Promise<MutateResult<TData>> => {
81
83
  const key = optionsRef.current.mutationKey;
82
- if (key && inFlightKeys.current.has(key)) return null;
84
+ if (key && inFlightKeys.current.has(key)) return { status: "skipped" };
83
85
  if (key) inFlightKeys.current.add(key);
84
86
 
85
- let retriesLeft = optionsRef.current.retry === true
86
- ? Infinity
87
- : (optionsRef.current.retry ?? 0);
87
+ let retriesLeft: number =
88
+ optionsRef.current.retry === true
89
+ ? Infinity
90
+ : typeof optionsRef.current.retry === "number"
91
+ ? optionsRef.current.retry
92
+ : 0;
88
93
 
89
94
  const mid = "mut-" + crypto.randomUUID();
90
95
  setMutationId(mid);
91
96
  trackMutation(mid, "pending");
92
- setState({
93
- data: null,
94
- error: null,
95
- isLoading: true,
96
- isError: false,
97
- isSuccess: false,
98
- });
97
+ setIsLoading(true);
99
98
 
100
99
  optionsRef.current.onMutate?.();
101
100
 
102
- const attempt = async (): Promise<TData | null> => {
101
+ const attempt = async (): Promise<MutateResult<TData>> => {
103
102
  try {
104
- const fetchOptions = { ...optionsRef.current, body };
105
- const { response, queued } = await fetchWithCache<TData>(urlRef.current, fetchOptions);
103
+ const {
104
+ mutationKey,
105
+ onMutate,
106
+ onSuccess,
107
+ onError,
108
+ onSettled,
109
+ retry,
110
+ ...fetchOnlyOptions
111
+ } = optionsRef.current;
112
+ const fetchOptions = { ...fetchOnlyOptions, body };
113
+ const { response, queued } = await fetchWithCache<TData>(
114
+ urlRef.current,
115
+ fetchOptions as FetchWithCacheOptions,
116
+ );
106
117
  if (queued) {
107
118
  resolveMutation(mid, null);
108
- setState({
109
- data: null,
110
- error: null,
111
- isLoading: false,
112
- isError: false,
113
- isSuccess: false,
114
- });
119
+ setIsLoading(false);
120
+ const r: MutateResult<TData> = { status: "queued" };
121
+ setLastResult(r);
115
122
  optionsRef.current.onSettled?.();
116
123
  callbacks?.onSettled?.();
117
124
  if (key) inFlightKeys.current.delete(key);
118
- return null;
125
+ return r;
119
126
  }
120
127
  const data: TData = await response.json();
121
128
  resolveMutation(mid, data);
122
- setState({
123
- data,
124
- error: null,
125
- isLoading: false,
126
- isError: false,
127
- isSuccess: true,
128
- });
129
+ setIsLoading(false);
130
+ const r: MutateResult<TData> = { status: "success", data };
131
+ setLastResult(r);
129
132
  optionsRef.current.onSuccess?.(data);
130
133
  callbacks?.onSuccess?.(data);
131
134
  optionsRef.current.onSettled?.();
132
135
  callbacks?.onSettled?.();
133
136
  if (key) inFlightKeys.current.delete(key);
134
- return data;
137
+ return r;
135
138
  } catch (err) {
136
139
  if (retriesLeft > 0) {
137
140
  retriesLeft--;
@@ -140,19 +143,15 @@ export function useMutation<TData = unknown>(
140
143
  }
141
144
  const error = err instanceof Error ? err : new Error(String(err));
142
145
  rejectMutation(mid, error);
143
- setState({
144
- data: null,
145
- error,
146
- isLoading: false,
147
- isError: true,
148
- isSuccess: false,
149
- });
146
+ setIsLoading(false);
147
+ const r: MutateResult<TData> = { status: "error", error };
148
+ setLastResult(r);
150
149
  optionsRef.current.onError?.(error);
151
150
  callbacks?.onError?.(error);
152
151
  optionsRef.current.onSettled?.();
153
152
  callbacks?.onSettled?.();
154
153
  if (key) inFlightKeys.current.delete(key);
155
- return null;
154
+ return r;
156
155
  }
157
156
  };
158
157
 
@@ -162,18 +161,20 @@ export function useMutation<TData = unknown>(
162
161
  );
163
162
 
164
163
  const reset = useCallback(() => {
165
- setState({
166
- data: null,
167
- error: null,
168
- isLoading: false,
169
- isError: false,
170
- isSuccess: false,
171
- });
164
+ setLastResult(null);
165
+ setIsLoading(false);
172
166
  }, []);
173
167
 
174
168
  return {
175
- ...state,
176
169
  mutate,
170
+ isLoading,
171
+ isIdle: lastResult === null,
172
+ isQueued: lastResult?.status === "queued",
173
+ isSuccess: lastResult?.status === "success",
174
+ isError: lastResult?.status === "error",
175
+ data: lastResult?.status === "success" ? (lastResult.data ?? null) : null,
176
+ error: lastResult?.status === "error" ? (lastResult.error ?? null) : null,
177
+ lastResult,
177
178
  reset,
178
179
  mutationId,
179
180
  };
@@ -1,5 +1,9 @@
1
1
  import { useState, useEffect, useCallback } from "react";
2
- import { getPendingCount, getQueueItems, processMutationQueue } from "../offline/queue.js";
2
+ import {
3
+ getPendingCount,
4
+ getQueueItems,
5
+ processMutationQueue,
6
+ } from "../mutation/queue.js";
3
7
 
4
8
  /**
5
9
  * Reactive mutation queue state: pending count, items, last sync info, and processing flag.
@@ -1,5 +1,9 @@
1
1
  import { useState, useEffect, useCallback } from "react";
2
- import { getPendingCount, getQueueItems, processMutationQueue } from "../offline/queue.ts";
2
+ import {
3
+ getPendingCount,
4
+ getQueueItems,
5
+ processMutationQueue,
6
+ } from "../mutation/queue.ts";
3
7
  import type { MutationQueueItem } from "../swoff.d.ts";
4
8
 
5
9
  export interface MutationQueueState {
@@ -25,7 +29,9 @@ export interface MutationQueueState {
25
29
  *
26
30
  * @returns {{ pending, items, lastSync, isProcessing, retryAll }}
27
31
  */
28
- export function useMutationQueue(): MutationQueueState & { retryAll: () => Promise<void> } {
32
+ export function useMutationQueue(): MutationQueueState & {
33
+ retryAll: () => Promise<void>;
34
+ } {
29
35
  const [state, setState] = useState<MutationQueueState>({
30
36
  pending: 0,
31
37
  items: [],
@@ -1,13 +1,12 @@
1
1
  import { useSyncExternalStore } from "react";
2
- import { getMutationState, onMutationStateChange } from "../mutation-state.js";
2
+ import { getMutationState, onMutationStateChange } from "../mutation/state.js";
3
3
 
4
4
  function subscribeToMutations(cb) {
5
5
  return onMutationStateChange(() => cb());
6
6
  }
7
7
 
8
8
  export function useMutationState(id) {
9
- return useSyncExternalStore(
10
- subscribeToMutations,
11
- () => (id ? getMutationState(id) ?? null : null),
9
+ return useSyncExternalStore(subscribeToMutations, () =>
10
+ id ? (getMutationState(id) ?? null) : null,
12
11
  );
13
12
  }
@@ -1,9 +1,6 @@
1
1
  import { useSyncExternalStore } from "react";
2
- import {
3
- getMutationState,
4
- onMutationStateChange,
5
- } from "../mutation-state.ts";
6
- import type { MutationState } from "../mutation-state.ts";
2
+ import { getMutationState, onMutationStateChange } from "../mutation/state.ts";
3
+ import type { MutationState } from "../mutation/state.ts";
7
4
 
8
5
  function subscribeToMutations(cb: () => void) {
9
6
  return onMutationStateChange(() => cb());
@@ -18,8 +15,7 @@ function subscribeToMutations(cb: () => void) {
18
15
  * if (mutation?.status === "error") { ... }
19
16
  */
20
17
  export function useMutationState(id: string | null): MutationState | null {
21
- return useSyncExternalStore(
22
- subscribeToMutations,
23
- () => (id ? getMutationState(id) ?? null : null),
18
+ return useSyncExternalStore(subscribeToMutations, () =>
19
+ id ? (getMutationState(id) ?? null) : null,
24
20
  );
25
21
  }
@@ -1,4 +1,9 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
+ import {
3
+ CONNECTIVITY_EVENT,
4
+ forceRetry,
5
+ getCurrentOnlineStatus,
6
+ } from "./connectivity.js";
2
7
 
3
8
  /**
4
9
  * Reactive network information: online status, connection type, and bandwidth.
@@ -14,48 +19,83 @@ import { useState, useEffect, useRef } from "react";
14
19
  */
15
20
  export function useNetworkStatus() {
16
21
  const wasOfflineRef = useRef(false);
22
+ const [isRetrying, setIsRetrying] = useState(false);
17
23
 
18
24
  const [state, setState] = useState(() => {
19
- const online = typeof navigator !== "undefined" ? navigator.onLine : true;
20
- const connection = navigator?.connection;
21
25
  return {
22
- online,
26
+ online: true,
23
27
  wasOffline: false,
24
28
  lastChangedAt: null,
25
- effectiveType: connection?.effectiveType ?? null,
26
- downlink: connection?.downlink ?? null,
29
+ effectiveType:
30
+ (typeof navigator !== "undefined" &&
31
+ navigator.connection?.effectiveType) ||
32
+ null,
33
+ downlink:
34
+ (typeof navigator !== "undefined" && navigator.connection?.downlink) ||
35
+ null,
27
36
  };
28
37
  });
29
38
 
39
+ // Manual trigger wrapper that handles local loading state
40
+ const retry = async () => {
41
+ setIsRetrying(true);
42
+ await forceRetry();
43
+ setIsRetrying(false);
44
+ };
45
+
30
46
  useEffect(() => {
31
- const onOnline = () => {
32
- setState((s) => ({ ...s, online: true, lastChangedAt: Date.now() }));
33
- };
34
- const onOffline = () => {
47
+ // Sync with actual status after mount — prevents SSR hydration mismatch
48
+ // where the server defaults to online:true but the browser is offline.
49
+ if (!getCurrentOnlineStatus()) {
35
50
  wasOfflineRef.current = true;
36
- setState((s) => ({ ...s, online: false, lastChangedAt: Date.now(), wasOffline: true }));
51
+ setState((s) => ({
52
+ ...s,
53
+ online: false,
54
+ wasOffline: true,
55
+ lastChangedAt: Date.now(),
56
+ }));
57
+ }
58
+
59
+ const connection = navigator.connection;
60
+
61
+ const handleConnectivityChange = (e) => {
62
+ const isTrulyOnline = e.detail.online;
63
+
64
+ if (!isTrulyOnline) {
65
+ wasOfflineRef.current = true;
66
+ } else {
67
+ wasOfflineRef.current = false;
68
+ }
69
+
70
+ setState((s) => ({
71
+ ...s,
72
+ online: isTrulyOnline,
73
+ wasOffline: wasOfflineRef.current,
74
+ lastChangedAt: Date.now(),
75
+ }));
37
76
  };
38
77
 
39
- const connection = navigator?.connection;
40
78
  const onTypeChange = () => {
41
79
  if (!connection) return;
42
- setState((s) => ({ ...s, effectiveType: connection.effectiveType, downlink: connection.downlink }));
80
+ setState((s) => ({
81
+ ...s,
82
+ effectiveType: connection.effectiveType,
83
+ downlink: connection.downlink,
84
+ }));
43
85
  };
44
86
 
45
- window.addEventListener("online", onOnline);
46
- window.addEventListener("offline", onOffline);
87
+ window.addEventListener(CONNECTIVITY_EVENT, handleConnectivityChange);
47
88
  if (connection) {
48
89
  connection.addEventListener("change", onTypeChange);
49
90
  }
50
91
 
51
92
  return () => {
52
- window.removeEventListener("online", onOnline);
53
- window.removeEventListener("offline", onOffline);
93
+ window.removeEventListener(CONNECTIVITY_EVENT, handleConnectivityChange);
54
94
  if (connection) {
55
95
  connection.removeEventListener("change", onTypeChange);
56
96
  }
57
97
  };
58
98
  }, []);
59
99
 
60
- return state;
100
+ return { ...state, isRetrying, retry };
61
101
  }
@@ -1,4 +1,9 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
+ import {
3
+ CONNECTIVITY_EVENT,
4
+ forceRetry,
5
+ getCurrentOnlineStatus,
6
+ } from "../connectivity.ts";
2
7
 
3
8
  /**
4
9
  * Reactive network information: online status, connection type, and bandwidth.
@@ -12,65 +17,82 @@ import { useState, useEffect, useRef } from "react";
12
17
  * // Warn on slow connection:
13
18
  * if (online && effectiveType === "2g") return <SlowConnectionWarning />;
14
19
  *
15
- * @returns {{ online: boolean, wasOffline: boolean, lastChangedAt: number | null, effectiveType: string | null, downlink: number | null }}
20
+ * @returns {{ online: boolean, wasOffline: boolean, lastChangedAt: number | null, effectiveType: string | null, downlink: number | null, isRetrying: boolean, retry: () => Promise<void> }}
16
21
  */
17
22
  export function useNetworkStatus() {
18
23
  const wasOfflineRef = useRef(false);
24
+ const [isRetrying, setIsRetrying] = useState(false);
19
25
 
20
26
  const [state, setState] = useState(() => {
21
- const online = typeof navigator !== "undefined" ? navigator.onLine : true;
22
- const connection = navigator?.connection;
23
27
  return {
24
- online,
28
+ online: true,
25
29
  wasOffline: false,
26
30
  lastChangedAt: null as number | null,
27
- effectiveType: connection?.effectiveType ?? null,
28
- downlink: connection?.downlink ?? null,
31
+ effectiveType: null as string | null,
32
+ downlink: null as number | null,
29
33
  };
30
34
  });
31
35
 
36
+ // Manual trigger wrapper that handles local loading state
37
+ const retry = async () => {
38
+ setIsRetrying(true);
39
+ await forceRetry();
40
+ setIsRetrying(false);
41
+ };
42
+
32
43
  useEffect(() => {
33
- const onOnline = () => {
44
+ // Sync with actual status after mount — prevents SSR hydration mismatch
45
+ // where the server defaults to online:true but the browser is offline.
46
+ if (!getCurrentOnlineStatus()) {
47
+ wasOfflineRef.current = true;
34
48
  setState((s) => ({
35
49
  ...s,
36
- online: true,
50
+ online: false,
51
+ wasOffline: true,
37
52
  lastChangedAt: Date.now(),
38
53
  }));
39
- };
40
- const onOffline = () => {
41
- wasOfflineRef.current = true;
54
+ }
55
+
56
+ const connection = navigator.connection;
57
+
58
+ const handleConnectivityChange = (e: Event) => {
59
+ const isTrulyOnline = (e as CustomEvent).detail.online;
60
+
61
+ if (!isTrulyOnline) {
62
+ wasOfflineRef.current = true;
63
+ } else {
64
+ wasOfflineRef.current = false;
65
+ }
66
+
42
67
  setState((s) => ({
43
68
  ...s,
44
- online: false,
69
+ online: isTrulyOnline,
70
+ wasOffline: wasOfflineRef.current,
45
71
  lastChangedAt: Date.now(),
46
- wasOffline: true,
47
72
  }));
48
73
  };
49
74
 
50
- const connection = navigator?.connection;
51
75
  const onTypeChange = () => {
52
76
  if (!connection) return;
53
- setState((s) => ({
77
+ setState((s: typeof state) => ({
54
78
  ...s,
55
79
  effectiveType: connection.effectiveType,
56
80
  downlink: connection.downlink,
57
81
  }));
58
82
  };
59
83
 
60
- window.addEventListener("online", onOnline);
61
- window.addEventListener("offline", onOffline);
84
+ window.addEventListener(CONNECTIVITY_EVENT, handleConnectivityChange);
62
85
  if (connection) {
63
86
  connection.addEventListener("change", onTypeChange);
64
87
  }
65
88
 
66
89
  return () => {
67
- window.removeEventListener("online", onOnline);
68
- window.removeEventListener("offline", onOffline);
90
+ window.removeEventListener(CONNECTIVITY_EVENT, handleConnectivityChange);
69
91
  if (connection) {
70
92
  connection.removeEventListener("change", onTypeChange);
71
93
  }
72
94
  };
73
95
  }, []);
74
96
 
75
- return state;
97
+ return { ...state, isRetrying, retry };
76
98
  }
@@ -0,0 +1,42 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+
3
+ /**
4
+ * Tracks offline navigation fallback events dispatched by the Service Worker.
5
+ *
6
+ * Usage:
7
+ * const { lastEvent, events } = useOfflineAnalytics();
8
+ *
9
+ * // Render an offline indicator when a fallback was served:
10
+ * if (lastEvent) {
11
+ * return <div>Offline: served {lastEvent.fallbackLevel} for {lastEvent.route}</div>;
12
+ * }
13
+ *
14
+ * // Or pass a callback:
15
+ * useOfflineAnalytics((event) => console.log("Fallback:", event));
16
+ */
17
+ export function useOfflineAnalytics(callback) {
18
+ const [events, setEvents] = useState([]);
19
+ const [lastEvent, setLastEvent] = useState(null);
20
+
21
+ const handler = useCallback((event) => {
22
+ if (event.data?.type === "OFFLINE_FALLBACK_ACTIVATED" && event.data?.detail) {
23
+ const detail = event.data.detail;
24
+ setEvents((prev) => [...prev, detail]);
25
+ setLastEvent(detail);
26
+ callback?.(detail);
27
+ }
28
+ }, [callback]);
29
+
30
+ useEffect(() => {
31
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return;
32
+ navigator.serviceWorker.addEventListener("message", handler);
33
+ return () => navigator.serviceWorker.removeEventListener("message", handler);
34
+ }, [handler]);
35
+
36
+ const clear = useCallback(() => {
37
+ setEvents([]);
38
+ setLastEvent(null);
39
+ }, []);
40
+
41
+ return { lastEvent, events, clear };
42
+ }
@@ -0,0 +1,50 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+
3
+ export interface OfflineFallbackEvent {
4
+ route: string;
5
+ fallbackLevel: "route-fallback" | "offline-page" | "spa-shell" | "inline-503";
6
+ timestamp: number;
7
+ }
8
+
9
+ /**
10
+ * Tracks offline navigation fallback events dispatched by the Service Worker.
11
+ *
12
+ * Usage:
13
+ * const { lastEvent, events } = useOfflineAnalytics();
14
+ *
15
+ * // Render an offline indicator when a fallback was served:
16
+ * if (lastEvent) {
17
+ * return <div>Offline: served {lastEvent.fallbackLevel} for {lastEvent.route}</div>;
18
+ * }
19
+ *
20
+ * // Or pass a callback:
21
+ * useOfflineAnalytics((event) => console.log("Fallback:", event));
22
+ *
23
+ * @returns {{ lastEvent: OfflineFallbackEvent | null, events: OfflineFallbackEvent[], clear: () => void }}
24
+ */
25
+ export function useOfflineAnalytics(callback?: (event: OfflineFallbackEvent) => void) {
26
+ const [events, setEvents] = useState<OfflineFallbackEvent[]>([]);
27
+ const [lastEvent, setLastEvent] = useState<OfflineFallbackEvent | null>(null);
28
+
29
+ const handler = useCallback((event: MessageEvent) => {
30
+ if (event.data?.type === "OFFLINE_FALLBACK_ACTIVATED" && event.data?.detail) {
31
+ const detail = event.data.detail as OfflineFallbackEvent;
32
+ setEvents((prev) => [...prev, detail]);
33
+ setLastEvent(detail);
34
+ callback?.(detail);
35
+ }
36
+ }, [callback]);
37
+
38
+ useEffect(() => {
39
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return;
40
+ navigator.serviceWorker.addEventListener("message", handler);
41
+ return () => navigator.serviceWorker.removeEventListener("message", handler);
42
+ }, [handler]);
43
+
44
+ const clear = useCallback(() => {
45
+ setEvents([]);
46
+ setLastEvent(null);
47
+ }, []);
48
+
49
+ return { lastEvent, events, clear };
50
+ }
@@ -0,0 +1,26 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { isInstallable, promptInstall } from "../pwa/prompt.js";
3
+
4
+ export function usePwaInstall() {
5
+ const [canInstall, setCanInstall] = useState(false);
6
+
7
+ useEffect(() => {
8
+ const onReady = () => setCanInstall(true);
9
+ const onDone = () => setCanInstall(false);
10
+
11
+ window.addEventListener("pwa-installable", onReady);
12
+ window.addEventListener("pwa-installed", onDone);
13
+ window.addEventListener("pwa-dismissed", onDone);
14
+ return () => {
15
+ window.removeEventListener("pwa-installable", onReady);
16
+ window.removeEventListener("pwa-installed", onDone);
17
+ window.removeEventListener("pwa-dismissed", onDone);
18
+ };
19
+ }, []);
20
+
21
+ const install = useCallback(async () => {
22
+ await promptInstall();
23
+ }, []);
24
+
25
+ return { canInstall, install };
26
+ }
@@ -0,0 +1,26 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { isInstallable, promptInstall } from "../pwa/prompt.ts";
3
+
4
+ export function usePwaInstall() {
5
+ const [canInstall, setCanInstall] = useState(false);
6
+
7
+ useEffect(() => {
8
+ const onReady = () => setCanInstall(true);
9
+ const onDone = () => setCanInstall(false);
10
+
11
+ window.addEventListener("pwa-installable", onReady);
12
+ window.addEventListener("pwa-installed", onDone);
13
+ window.addEventListener("pwa-dismissed", onDone);
14
+ return () => {
15
+ window.removeEventListener("pwa-installable", onReady);
16
+ window.removeEventListener("pwa-installed", onDone);
17
+ window.removeEventListener("pwa-dismissed", onDone);
18
+ };
19
+ }, []);
20
+
21
+ const install = useCallback(async () => {
22
+ await promptInstall();
23
+ }, []);
24
+
25
+ return { canInstall, install };
26
+ }