@swoff/cli 0.3.7 → 0.3.8

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 (66) hide show
  1. package/README.md +43 -610
  2. package/dist/__tests__/assemble-sw.test.js +4 -4
  3. package/dist/__tests__/assemble-sw.test.js.map +1 -1
  4. package/dist/__tests__/config-types.test.js +7 -4
  5. package/dist/__tests__/config-types.test.js.map +1 -1
  6. package/dist/__tests__/validator.test.js +50 -1
  7. package/dist/__tests__/validator.test.js.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/commands/add.js +5 -4
  11. package/dist/lib/commands/add.js.map +1 -1
  12. package/dist/lib/commands/generate-guide.js +3 -3
  13. package/dist/lib/commands/generate-guide.js.map +1 -1
  14. package/dist/lib/commands/info.js +29 -6
  15. package/dist/lib/commands/info.js.map +1 -1
  16. package/dist/lib/config/validator.js +85 -4
  17. package/dist/lib/config/validator.js.map +1 -1
  18. package/dist/lib/generators/file-generators/client-injector.js +1 -1
  19. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  20. package/dist/lib/generators/file-generators/fetch-wrapper.js +55 -14
  21. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  22. package/dist/lib/generators/file-generators/generate-hooks.js +5 -1
  23. package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
  24. package/dist/lib/generators/file-generators/gql-wrapper.js +152 -0
  25. package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -0
  26. package/dist/lib/generators/file-generators/guide-generator.js +214 -10
  27. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
  28. package/dist/lib/generators/file-generators/mutation-queue.js +117 -49
  29. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  30. package/dist/lib/generators/file-generators/mutation-state.js +126 -0
  31. package/dist/lib/generators/file-generators/mutation-state.js.map +1 -0
  32. package/dist/lib/generators/file-generators/quick-readme.js +59 -9
  33. package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
  34. package/dist/lib/generators/file-generators/server-push.js +136 -0
  35. package/dist/lib/generators/file-generators/server-push.js.map +1 -0
  36. package/dist/lib/generators/file-generators/type-definitions.js +28 -0
  37. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
  38. package/dist/lib/generators/sw-sections/assemble-sw.js +6 -1
  39. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  40. package/dist/lib/generators/sw-sections/background-sync-handler.js +22 -8
  41. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
  42. package/dist/lib/generators/sw-sections/config-header.js +1 -1
  43. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  44. package/dist/lib/generators/sw-sections/default-template.js +2 -1
  45. package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
  46. package/dist/lib/generators/sw-sections/fetch-handler.js +137 -98
  47. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  48. package/dist/lib/generators/sw-sections/server-push-handler.js +114 -0
  49. package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -0
  50. package/dist/lib/generators/sw-sections/tag-management.js +6 -5
  51. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  52. package/dist/lib/generators/swoff-files-generator.js +7 -1
  53. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  54. package/dist/lib/shared/config-types.js +40 -2
  55. package/dist/lib/shared/config-types.js.map +1 -1
  56. package/package.json +1 -1
  57. package/templates/hooks/useCachedFetch.jsx +43 -4
  58. package/templates/hooks/useCachedFetch.tsx +49 -6
  59. package/templates/hooks/useMutation.jsx +76 -0
  60. package/templates/hooks/useMutation.tsx +97 -0
  61. package/templates/hooks/useMutationQueue.jsx +19 -11
  62. package/templates/hooks/useMutationQueue.tsx +30 -15
  63. package/templates/hooks/useMutationState.jsx +27 -0
  64. package/templates/hooks/useMutationState.tsx +41 -0
  65. package/templates/hooks/usePrefetch.jsx +10 -0
  66. package/templates/hooks/usePrefetch.tsx +25 -0
@@ -1 +1 @@
1
- {"version":3,"file":"config-types.js","sourceRoot":"","sources":["../../../src/lib/shared/config-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config-types.js","sourceRoot":"","sources":["../../../src/lib/shared/config-types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swoff/cli",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "CLI for Swoff - Offline-first web apps made easy",
6
6
  "bin": {
@@ -12,13 +12,23 @@ export function useCachedFetch(url, options = {}) {
12
12
 
13
13
  const refetch = useCallback(() => setRefetchCount((c) => c + 1), []);
14
14
 
15
+ const isEnabled = options.enabled !== false && url != null;
16
+
15
17
  useEffect(() => {
18
+ if (!isEnabled) {
19
+ setLoading(false);
20
+ return;
21
+ }
16
22
  let cancelled = false;
23
+ const controller = new AbortController();
17
24
  startTransition(() => setLoading(true));
18
25
 
19
26
  const doFetch = async () => {
20
27
  try {
21
- const { response } = await fetchWithCache(url, optionsRef.current);
28
+ const { response } = await fetchWithCache(url, {
29
+ ...optionsRef.current,
30
+ signal: controller.signal,
31
+ });
22
32
  if (cancelled) return;
23
33
  if (response) {
24
34
  setData(await response.json());
@@ -27,6 +37,7 @@ export function useCachedFetch(url, options = {}) {
27
37
  }
28
38
  if (!cancelled) setError(null);
29
39
  } catch (err) {
40
+ if (!cancelled && err instanceof DOMException && err.name === "AbortError") return;
30
41
  if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));
31
42
  } finally {
32
43
  if (!cancelled) setLoading(false);
@@ -34,10 +45,15 @@ export function useCachedFetch(url, options = {}) {
34
45
  };
35
46
 
36
47
  doFetch();
37
- return () => { cancelled = true; };
38
- }, [url, refetchCount]);
48
+ return () => {
49
+ cancelled = true;
50
+ controller.abort();
51
+ };
52
+ }, [url, refetchCount, isEnabled]);
39
53
 
54
+ // Auto-refetch on cache invalidation
40
55
  useEffect(() => {
56
+ if (!isEnabled) return;
41
57
  const onInvalidated = (e) => {
42
58
  const detail = e.detail;
43
59
  const tags = detail?.tags;
@@ -49,7 +65,30 @@ export function useCachedFetch(url, options = {}) {
49
65
  };
50
66
  window.addEventListener("cache-invalidated", onInvalidated);
51
67
  return () => window.removeEventListener("cache-invalidated", onInvalidated);
52
- }, [url]);
68
+ }, [url, isEnabled]);
69
+
70
+ // Auto-refetch on window focus
71
+ useEffect(() => {
72
+ if (!options.refetchOnWindowFocus || !isEnabled) return;
73
+ const onFocus = () => refetch();
74
+ window.addEventListener("focus", onFocus);
75
+ return () => window.removeEventListener("focus", onFocus);
76
+ }, [options.refetchOnWindowFocus, refetch, isEnabled]);
77
+
78
+ // Auto-refetch on reconnect
79
+ useEffect(() => {
80
+ if (!options.refetchOnReconnect || !isEnabled) return;
81
+ const onOnline = () => refetch();
82
+ window.addEventListener("online", onOnline);
83
+ return () => window.removeEventListener("online", onOnline);
84
+ }, [options.refetchOnReconnect, refetch, isEnabled]);
85
+
86
+ // Auto-refetch on interval
87
+ useEffect(() => {
88
+ if (!options.refetchInterval || options.refetchInterval <= 0 || !isEnabled) return;
89
+ const id = setInterval(refetch, options.refetchInterval * 1000);
90
+ return () => clearInterval(id);
91
+ }, [options.refetchInterval, refetch, isEnabled]);
53
92
 
54
93
  return { data, error, loading, refetch };
55
94
  }
@@ -7,10 +7,16 @@ import {
7
7
  } from "react";
8
8
  import { fetchWithCache } from "../fetch-wrapper.ts";
9
9
  import { generateTags } from "../invalidation-tags.ts";
10
+ import type { FetchWithCacheOptions } from "../fetch-wrapper.ts";
10
11
 
11
12
  export function useCachedFetch<T>(
12
- url: string,
13
- options: Parameters<typeof fetchWithCache>[1] = {},
13
+ url: string | null,
14
+ options: FetchWithCacheOptions & {
15
+ refetchOnWindowFocus?: boolean;
16
+ refetchOnReconnect?: boolean;
17
+ refetchInterval?: number;
18
+ enabled?: boolean;
19
+ } = {},
14
20
  ) {
15
21
  const [data, setData] = useState<T | null>(null);
16
22
  const [error, setError] = useState<Error | null>(null);
@@ -23,13 +29,23 @@ export function useCachedFetch<T>(
23
29
 
24
30
  const refetch = useCallback(() => setRefetchCount((c) => c + 1), []);
25
31
 
32
+ const isEnabled = options.enabled !== false && url != null;
33
+
26
34
  useEffect(() => {
35
+ if (!isEnabled) {
36
+ setLoading(false);
37
+ return;
38
+ }
27
39
  let cancelled = false;
40
+ const controller = new AbortController();
28
41
  startTransition(() => setLoading(true));
29
42
 
30
43
  const doFetch = async () => {
31
44
  try {
32
- const { response } = await fetchWithCache(url, optionsRef.current);
45
+ const { response } = await fetchWithCache(url, {
46
+ ...optionsRef.current,
47
+ signal: controller.signal,
48
+ });
33
49
  if (cancelled) return;
34
50
  if (response) {
35
51
  setData(await response.json());
@@ -38,6 +54,7 @@ export function useCachedFetch<T>(
38
54
  }
39
55
  if (!cancelled) setError(null);
40
56
  } catch (err) {
57
+ if (!cancelled && err instanceof DOMException && err.name === "AbortError") return;
41
58
  if (!cancelled)
42
59
  setError(err instanceof Error ? err : new Error(String(err)));
43
60
  } finally {
@@ -48,22 +65,48 @@ export function useCachedFetch<T>(
48
65
  doFetch();
49
66
  return () => {
50
67
  cancelled = true;
68
+ controller.abort();
51
69
  };
52
- }, [url, refetchCount]);
70
+ }, [url, refetchCount, isEnabled]);
53
71
 
72
+ // Auto-refetch on cache invalidation
54
73
  useEffect(() => {
74
+ if (!isEnabled) return;
55
75
  const onInvalidated = (e: Event) => {
56
76
  const detail = (e as CustomEvent).detail;
57
77
  const tags = detail?.tags;
58
78
  if (!tags || !Array.isArray(tags)) return;
59
- const urlTags = generateTags(url);
79
+ const urlTags = generateTags(url!);
60
80
  if (tags.some((t: string) => urlTags.includes(t))) {
61
81
  setRefetchCount((c) => c + 1);
62
82
  }
63
83
  };
64
84
  window.addEventListener("cache-invalidated", onInvalidated);
65
85
  return () => window.removeEventListener("cache-invalidated", onInvalidated);
66
- }, [url]);
86
+ }, [url, isEnabled]);
87
+
88
+ // Auto-refetch on window focus
89
+ useEffect(() => {
90
+ if (!options.refetchOnWindowFocus || !isEnabled) return;
91
+ const onFocus = () => refetch();
92
+ window.addEventListener("focus", onFocus);
93
+ return () => window.removeEventListener("focus", onFocus);
94
+ }, [options.refetchOnWindowFocus, refetch, isEnabled]);
95
+
96
+ // Auto-refetch on reconnect
97
+ useEffect(() => {
98
+ if (!options.refetchOnReconnect || !isEnabled) return;
99
+ const onOnline = () => refetch();
100
+ window.addEventListener("online", onOnline);
101
+ return () => window.removeEventListener("online", onOnline);
102
+ }, [options.refetchOnReconnect, refetch, isEnabled]);
103
+
104
+ // Auto-refetch on interval
105
+ useEffect(() => {
106
+ if (!options.refetchInterval || options.refetchInterval <= 0 || !isEnabled) return;
107
+ const id = setInterval(refetch, options.refetchInterval * 1000);
108
+ return () => clearInterval(id);
109
+ }, [options.refetchInterval, refetch, isEnabled]);
67
110
 
68
111
  return { data, error, loading, refetch };
69
112
  }
@@ -0,0 +1,76 @@
1
+ import { useEffect, useState, useCallback, useRef } from "react";
2
+ import { fetchWithCache } from "../fetch-wrapper.js";
3
+ import {
4
+ trackMutation,
5
+ resolveMutation,
6
+ rejectMutation,
7
+ } from "../mutation-state.js";
8
+
9
+ export function useMutation(options = {}) {
10
+ const [state, setState] = useState({
11
+ data: null,
12
+ error: null,
13
+ isLoading: false,
14
+ isError: false,
15
+ isSuccess: false,
16
+ });
17
+
18
+ const optionsRef = useRef(options);
19
+
20
+ useEffect(() => {
21
+ optionsRef.current = options;
22
+ }, [options]);
23
+
24
+ const mutate = useCallback(async (url, fetchOptions = {}) => {
25
+ const mutationId = "mut-" + crypto.randomUUID();
26
+ trackMutation(mutationId, "pending");
27
+ setState({
28
+ data: null,
29
+ error: null,
30
+ isLoading: true,
31
+ isError: false,
32
+ isSuccess: false,
33
+ });
34
+
35
+ try {
36
+ const { response } = await fetchWithCache(url, fetchOptions);
37
+ const data = await response.json();
38
+ resolveMutation(mutationId, data);
39
+ setState({
40
+ data,
41
+ error: null,
42
+ isLoading: false,
43
+ isError: false,
44
+ isSuccess: true,
45
+ });
46
+ optionsRef.current.onSuccess?.(data);
47
+ optionsRef.current.onSettled?.();
48
+ return data;
49
+ } catch (err) {
50
+ const error = err instanceof Error ? err : new Error(String(err));
51
+ rejectMutation(mutationId, error);
52
+ setState({
53
+ data: null,
54
+ error,
55
+ isLoading: false,
56
+ isError: true,
57
+ isSuccess: false,
58
+ });
59
+ optionsRef.current.onError?.(error);
60
+ optionsRef.current.onSettled?.();
61
+ return null;
62
+ }
63
+ }, []);
64
+
65
+ const reset = useCallback(() => {
66
+ setState({
67
+ data: null,
68
+ error: null,
69
+ isLoading: false,
70
+ isError: false,
71
+ isSuccess: false,
72
+ });
73
+ }, []);
74
+
75
+ return { ...state, mutate, reset };
76
+ }
@@ -0,0 +1,97 @@
1
+ import { useEffect, useState, useCallback, useRef } from "react";
2
+ import { fetchWithCache } from "../fetch-wrapper.ts";
3
+ import {
4
+ trackMutation,
5
+ resolveMutation,
6
+ rejectMutation,
7
+ } from "../mutation-state.ts";
8
+ import type { FetchWithCacheOptions } from "../fetch-wrapper.ts";
9
+
10
+ export interface UseMutationOptions<TData> {
11
+ onSuccess?: (data: TData) => void;
12
+ onError?: (error: Error) => void;
13
+ onSettled?: () => void;
14
+ }
15
+
16
+ export function useMutation<TData = unknown>(
17
+ options: UseMutationOptions<TData> = {},
18
+ ) {
19
+ const [state, setState] = useState<{
20
+ data: TData | null;
21
+ error: Error | null;
22
+ isLoading: boolean;
23
+ isError: boolean;
24
+ isSuccess: boolean;
25
+ }>({
26
+ data: null,
27
+ error: null,
28
+ isLoading: false,
29
+ isError: false,
30
+ isSuccess: false,
31
+ });
32
+
33
+ const optionsRef = useRef(options);
34
+
35
+ useEffect(() => {
36
+ optionsRef.current = options;
37
+ }, [options]);
38
+
39
+ const mutate = useCallback(
40
+ async (
41
+ url: string,
42
+ fetchOptions: FetchWithCacheOptions = {},
43
+ ): Promise<TData | null> => {
44
+ const mutationId = "mut-" + crypto.randomUUID();
45
+ trackMutation(mutationId, "pending");
46
+ setState({
47
+ data: null,
48
+ error: null,
49
+ isLoading: true,
50
+ isError: false,
51
+ isSuccess: false,
52
+ });
53
+
54
+ try {
55
+ const { response } = await fetchWithCache<TData>(url, fetchOptions);
56
+ const data: TData = await response.json();
57
+ resolveMutation(mutationId, data);
58
+ setState({
59
+ data,
60
+ error: null,
61
+ isLoading: false,
62
+ isError: false,
63
+ isSuccess: true,
64
+ });
65
+ optionsRef.current.onSuccess?.(data);
66
+ optionsRef.current.onSettled?.();
67
+ return data;
68
+ } catch (err) {
69
+ const error = err instanceof Error ? err : new Error(String(err));
70
+ rejectMutation(mutationId, error);
71
+ setState({
72
+ data: null,
73
+ error,
74
+ isLoading: false,
75
+ isError: true,
76
+ isSuccess: false,
77
+ });
78
+ optionsRef.current.onError?.(error);
79
+ optionsRef.current.onSettled?.();
80
+ return null;
81
+ }
82
+ },
83
+ [],
84
+ );
85
+
86
+ const reset = useCallback(() => {
87
+ setState({
88
+ data: null,
89
+ error: null,
90
+ isLoading: false,
91
+ isError: false,
92
+ isSuccess: false,
93
+ });
94
+ }, []);
95
+
96
+ return { ...state, mutate, reset };
97
+ }
@@ -1,25 +1,33 @@
1
- import { useState, useEffect } from "react";
2
- import { getPendingCount } from "../mutation-queue.js";
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { getPendingCount, getQueueItems } from "../mutation-queue.js";
3
3
 
4
4
  export function useMutationQueue() {
5
5
  const [state, setState] = useState({
6
6
  pending: 0,
7
+ items: [],
7
8
  lastSync: null,
8
9
  });
9
10
 
11
+ const refresh = useCallback(async () => {
12
+ const [count, items] = await Promise.all([
13
+ getPendingCount(),
14
+ getQueueItems(),
15
+ ]);
16
+ setState((s) => ({ ...s, pending: count, items }));
17
+ }, []);
18
+
10
19
  useEffect(() => {
11
- getPendingCount().then((count) => setState((s) => ({ ...s, pending: count })));
20
+ refresh();
12
21
 
13
22
  const onSync = (e) => {
14
- getPendingCount().then((count) => {
15
- setState({
16
- pending: count,
17
- lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
18
- });
19
- });
23
+ refresh();
24
+ setState((s) => ({
25
+ ...s,
26
+ lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
27
+ }));
20
28
  };
21
29
  const onChange = () => {
22
- getPendingCount().then((count) => setState((s) => ({ ...s, pending: count })));
30
+ refresh();
23
31
  };
24
32
 
25
33
  window.addEventListener("mutation-sync-complete", onSync);
@@ -28,7 +36,7 @@ export function useMutationQueue() {
28
36
  window.removeEventListener("mutation-sync-complete", onSync);
29
37
  window.removeEventListener("mutation-queue-changed", onChange);
30
38
  };
31
- }, []);
39
+ }, [refresh]);
32
40
 
33
41
  return state;
34
42
  }
@@ -1,25 +1,40 @@
1
- import { useState, useEffect } from "react";
2
- import { getPendingCount } from "../mutation-queue.ts";
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { getPendingCount, getQueueItems } from "../mutation-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
+ }
3
10
 
4
11
  export function useMutationQueue() {
5
- const [state, setState] = useState<{
6
- pending: number;
7
- lastSync: { succeeded: number; failed: number } | null;
8
- }>({ pending: 0, lastSync: null });
12
+ const [state, setState] = useState<MutationQueueState>({
13
+ pending: 0,
14
+ items: [],
15
+ lastSync: null,
16
+ });
17
+
18
+ const refresh = useCallback(async () => {
19
+ const [count, items] = await Promise.all([
20
+ getPendingCount(),
21
+ getQueueItems(),
22
+ ]);
23
+ setState((s) => ({ ...s, pending: count, items }));
24
+ }, []);
9
25
 
10
26
  useEffect(() => {
11
- getPendingCount().then((count: number) => setState((s) => ({ ...s, pending: count })));
27
+ refresh();
12
28
 
13
29
  const onSync = (e: CustomEvent) => {
14
- getPendingCount().then((count: number) => {
15
- setState({
16
- pending: count,
17
- lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
18
- });
19
- });
30
+ refresh();
31
+ setState((s) => ({
32
+ ...s,
33
+ lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
34
+ }));
20
35
  };
21
36
  const onChange = () => {
22
- getPendingCount().then((count: number) => setState((s) => ({ ...s, pending: count })));
37
+ refresh();
23
38
  };
24
39
 
25
40
  window.addEventListener("mutation-sync-complete", onSync);
@@ -28,7 +43,7 @@ export function useMutationQueue() {
28
43
  window.removeEventListener("mutation-sync-complete", onSync);
29
44
  window.removeEventListener("mutation-queue-changed", onChange);
30
45
  };
31
- }, []);
46
+ }, [refresh]);
32
47
 
33
48
  return state;
34
49
  }
@@ -0,0 +1,27 @@
1
+ import { useState, useEffect } from "react";
2
+ import { getMutationState, onMutationStateChange } from "../mutation-state.js";
3
+
4
+ export function useMutationState(id) {
5
+ const [state, setState] = useState(() =>
6
+ id ? getMutationState(id) ?? null : null,
7
+ );
8
+
9
+ useEffect(() => {
10
+ if (!id) {
11
+ setState(null);
12
+ return;
13
+ }
14
+
15
+ setState(getMutationState(id) ?? null);
16
+
17
+ const unsub = onMutationStateChange((updated) => {
18
+ if (updated.id === id) {
19
+ setState(updated);
20
+ }
21
+ });
22
+
23
+ return unsub;
24
+ }, [id]);
25
+
26
+ return state;
27
+ }
@@ -0,0 +1,41 @@
1
+ import { useState, useEffect } from "react";
2
+ import {
3
+ getMutationState,
4
+ onMutationStateChange,
5
+ } from "../mutation-state.ts";
6
+ import type { MutationState } from "../mutation-state.ts";
7
+
8
+ /**
9
+ * Hook that subscribes to a specific mutation's state changes.
10
+ * Useful for showing per-mutation loading spinners, error states, etc.
11
+ *
12
+ * Usage:
13
+ * const mutation = useMutationState(mutationId);
14
+ * if (mutation?.status === "error") { ... }
15
+ */
16
+ export function useMutationState(id: string | null): MutationState | null {
17
+ const [state, setState] = useState<MutationState | null>(() =>
18
+ id ? getMutationState(id) ?? null : null,
19
+ );
20
+
21
+ useEffect(() => {
22
+ if (!id) {
23
+ setState(null);
24
+ return;
25
+ }
26
+
27
+ // Initial state
28
+ setState(getMutationState(id) ?? null);
29
+
30
+ // Subscribe to changes
31
+ const unsub = onMutationStateChange((updated) => {
32
+ if (updated.id === id) {
33
+ setState(updated);
34
+ }
35
+ });
36
+
37
+ return unsub;
38
+ }, [id]);
39
+
40
+ return state;
41
+ }
@@ -0,0 +1,10 @@
1
+ import { useCallback } from "react";
2
+ import { prefetchCache } from "../fetch-wrapper.js";
3
+
4
+ export function usePrefetch() {
5
+ const prefetch = useCallback((url, options) => {
6
+ prefetchCache(url, options);
7
+ }, []);
8
+
9
+ return prefetch;
10
+ }
@@ -0,0 +1,25 @@
1
+ import { useCallback } from "react";
2
+ import { prefetchCache } from "../fetch-wrapper.ts";
3
+ import type { FetchWithCacheOptions } from "../fetch-wrapper.ts";
4
+
5
+ /**
6
+ * Hook that returns a stable prefetch callback for warming the SW cache.
7
+ * Useful for route-level prefetching, link hover prefetching, etc.
8
+ *
9
+ * Usage:
10
+ * const prefetch = usePrefetch();
11
+ * <Link onMouseEnter={() => prefetch("/api/todos")} to="/todos" />
12
+ */
13
+ export function usePrefetch(): (
14
+ url: string,
15
+ options?: FetchWithCacheOptions,
16
+ ) => void {
17
+ const prefetch = useCallback(
18
+ (url: string, options?: FetchWithCacheOptions) => {
19
+ prefetchCache(url, options);
20
+ },
21
+ [],
22
+ );
23
+
24
+ return prefetch;
25
+ }