@swoff/cli 0.3.7 → 0.3.9
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.
- package/README.md +53 -606
- package/dist/__tests__/assemble-sw.test.js +8 -38
- package/dist/__tests__/assemble-sw.test.js.map +1 -1
- package/dist/__tests__/cli-integration.test.js +119 -11
- package/dist/__tests__/cli-integration.test.js.map +1 -1
- package/dist/__tests__/config-types.test.js +38 -18
- package/dist/__tests__/config-types.test.js.map +1 -1
- package/dist/__tests__/file-generators.test.js +2 -2
- package/dist/__tests__/file-generators.test.js.map +1 -1
- package/dist/__tests__/glob-matcher.test.js +86 -0
- package/dist/__tests__/glob-matcher.test.js.map +1 -0
- package/dist/__tests__/invalidation-tags.test.js +207 -23
- package/dist/__tests__/invalidation-tags.test.js.map +1 -1
- package/dist/__tests__/loader.test.js +12 -12
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/validator.test.js +170 -74
- package/dist/__tests__/validator.test.js.map +1 -1
- package/dist/index.js +12 -11
- package/dist/index.js.map +1 -1
- package/dist/lib/cli/help.js +0 -5
- package/dist/lib/cli/help.js.map +1 -1
- package/dist/lib/commands/add.js +52 -33
- package/dist/lib/commands/add.js.map +1 -1
- package/dist/lib/commands/clean.js +1 -15
- package/dist/lib/commands/clean.js.map +1 -1
- package/dist/lib/commands/generate-guide.js +5 -5
- package/dist/lib/commands/generate-guide.js.map +1 -1
- package/dist/lib/commands/generate.js +7 -21
- package/dist/lib/commands/generate.js.map +1 -1
- package/dist/lib/commands/info.js +37 -12
- package/dist/lib/commands/info.js.map +1 -1
- package/dist/lib/commands/init.js +2 -10
- package/dist/lib/commands/init.js.map +1 -1
- package/dist/lib/commands/validate.js +3 -3
- package/dist/lib/commands/validate.js.map +1 -1
- package/dist/lib/config/loader.js +3 -6
- package/dist/lib/config/loader.js.map +1 -1
- package/dist/lib/config/validator.js +192 -42
- package/dist/lib/config/validator.js.map +1 -1
- package/dist/lib/generators/file-generators/cache.js +2 -10
- package/dist/lib/generators/file-generators/cache.js.map +1 -1
- package/dist/lib/generators/file-generators/client-injector.js +51 -12
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-wrapper.js +127 -58
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-hooks.js +6 -2
- package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
- package/dist/lib/generators/file-generators/gql-wrapper.js +139 -0
- package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -0
- package/dist/lib/generators/file-generators/guide-generator.js +6 -436
- package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
- package/dist/lib/generators/file-generators/invalidation-tags.js +206 -19
- package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
- package/dist/lib/generators/file-generators/manifest.js +2 -2
- package/dist/lib/generators/file-generators/manifest.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-queue.js +140 -52
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-state.js +126 -0
- package/dist/lib/generators/file-generators/mutation-state.js.map +1 -0
- package/dist/lib/generators/file-generators/push.js +6 -7
- package/dist/lib/generators/file-generators/push.js.map +1 -1
- package/dist/lib/generators/file-generators/quick-readme.js +8 -75
- package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
- package/dist/lib/generators/file-generators/server-push.js +146 -0
- package/dist/lib/generators/file-generators/server-push.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-generator-build.js +8 -9
- package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-injector.js +2 -1
- package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/type-definitions.js +26 -1
- package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
- package/dist/lib/generators/sw-generator.js +8 -18
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/assemble-sw.js +19 -11
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/background-sync-handler.js +80 -24
- package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/config-header.js +2 -2
- package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
- package/dist/lib/generators/sw-sections/default-template.js +2 -1
- package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
- package/dist/lib/generators/sw-sections/fetch-handler.js +461 -121
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/message-handler.js +55 -15
- package/dist/lib/generators/sw-sections/message-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/server-push-handler.js +114 -0
- package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/sw-push.js +67 -0
- package/dist/lib/generators/sw-sections/sw-push.js.map +1 -0
- package/dist/lib/generators/sw-sections/tag-management.js +62 -21
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +12 -16
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +133 -45
- package/dist/lib/shared/config-types.js.map +1 -1
- package/dist/lib/shared/glob-matcher.js +77 -0
- package/dist/lib/shared/glob-matcher.js.map +1 -0
- package/dist/lib/utils/tty-status.js +21 -0
- package/dist/lib/utils/tty-status.js.map +1 -0
- package/docs/API.md +565 -0
- package/docs/ARCHITECTURE.md +374 -0
- package/docs/CLI.md +148 -0
- package/docs/COMPARISON.md +192 -0
- package/docs/CONFIG.md +277 -0
- package/docs/ECOSYSTEM.md +33 -0
- package/package.json +2 -1
- package/templates/hooks/useAuth.jsx +2 -2
- package/templates/hooks/useAuth.tsx +2 -2
- package/templates/hooks/useCachedFetch.jsx +21 -7
- package/templates/hooks/useCachedFetch.tsx +23 -11
- package/templates/hooks/useMutation.jsx +76 -0
- package/templates/hooks/useMutation.tsx +109 -0
- package/templates/hooks/useMutationQueue.jsx +20 -11
- package/templates/hooks/useMutationQueue.tsx +31 -15
- package/templates/hooks/useMutationState.jsx +13 -0
- package/templates/hooks/useMutationState.tsx +25 -0
- package/templates/hooks/usePrefetch.jsx +10 -0
- package/templates/hooks/usePrefetch.tsx +25 -0
- package/templates/hooks/usePushSubscription.jsx +3 -3
- package/templates/hooks/usePushSubscription.tsx +3 -3
|
@@ -2,34 +2,42 @@ import {
|
|
|
2
2
|
useState,
|
|
3
3
|
useEffect,
|
|
4
4
|
useCallback,
|
|
5
|
-
useRef,
|
|
6
5
|
startTransition,
|
|
7
6
|
} from "react";
|
|
8
7
|
import { fetchWithCache } from "../fetch-wrapper.ts";
|
|
9
8
|
import { generateTags } from "../invalidation-tags.ts";
|
|
9
|
+
import type { FetchWithCacheOptions } from "../fetch-wrapper.ts";
|
|
10
10
|
|
|
11
11
|
export function useCachedFetch<T>(
|
|
12
|
-
url: string,
|
|
13
|
-
options:
|
|
12
|
+
url: string | null,
|
|
13
|
+
options: FetchWithCacheOptions & {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
} = {},
|
|
14
16
|
) {
|
|
15
17
|
const [data, setData] = useState<T | null>(null);
|
|
16
18
|
const [error, setError] = useState<Error | null>(null);
|
|
17
19
|
const [loading, setLoading] = useState(true);
|
|
18
20
|
const [refetchCount, setRefetchCount] = useState(0);
|
|
19
|
-
const optionsRef = useRef(options);
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
optionsRef.current = options;
|
|
22
|
-
}, [options]);
|
|
23
21
|
|
|
24
22
|
const refetch = useCallback(() => setRefetchCount((c) => c + 1), []);
|
|
25
23
|
|
|
24
|
+
const isEnabled = options.enabled !== false && url != null;
|
|
25
|
+
|
|
26
26
|
useEffect(() => {
|
|
27
|
+
if (!isEnabled) {
|
|
28
|
+
startTransition(() => setLoading(false));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
27
31
|
let cancelled = false;
|
|
32
|
+
const controller = new AbortController();
|
|
28
33
|
startTransition(() => setLoading(true));
|
|
29
34
|
|
|
30
35
|
const doFetch = async () => {
|
|
31
36
|
try {
|
|
32
|
-
const { response } = await fetchWithCache(url,
|
|
37
|
+
const { response } = await fetchWithCache(url, {
|
|
38
|
+
...options,
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
});
|
|
33
41
|
if (cancelled) return;
|
|
34
42
|
if (response) {
|
|
35
43
|
setData(await response.json());
|
|
@@ -38,6 +46,7 @@ export function useCachedFetch<T>(
|
|
|
38
46
|
}
|
|
39
47
|
if (!cancelled) setError(null);
|
|
40
48
|
} catch (err) {
|
|
49
|
+
if (!cancelled && err instanceof DOMException && err.name === "AbortError") return;
|
|
41
50
|
if (!cancelled)
|
|
42
51
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
43
52
|
} finally {
|
|
@@ -48,22 +57,25 @@ export function useCachedFetch<T>(
|
|
|
48
57
|
doFetch();
|
|
49
58
|
return () => {
|
|
50
59
|
cancelled = true;
|
|
60
|
+
controller.abort();
|
|
51
61
|
};
|
|
52
|
-
}, [url, refetchCount]);
|
|
62
|
+
}, [url, refetchCount, isEnabled]);
|
|
53
63
|
|
|
64
|
+
// Auto-refetch on cache invalidation
|
|
54
65
|
useEffect(() => {
|
|
66
|
+
if (!isEnabled) return;
|
|
55
67
|
const onInvalidated = (e: Event) => {
|
|
56
68
|
const detail = (e as CustomEvent).detail;
|
|
57
69
|
const tags = detail?.tags;
|
|
58
70
|
if (!tags || !Array.isArray(tags)) return;
|
|
59
|
-
const urlTags = generateTags(url);
|
|
71
|
+
const urlTags = generateTags(url!);
|
|
60
72
|
if (tags.some((t: string) => urlTags.includes(t))) {
|
|
61
73
|
setRefetchCount((c) => c + 1);
|
|
62
74
|
}
|
|
63
75
|
};
|
|
64
76
|
window.addEventListener("cache-invalidated", onInvalidated);
|
|
65
77
|
return () => window.removeEventListener("cache-invalidated", onInvalidated);
|
|
66
|
-
}, [url]);
|
|
78
|
+
}, [url, isEnabled]);
|
|
67
79
|
|
|
68
80
|
return { data, error, loading, refetch };
|
|
69
81
|
}
|
|
@@ -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,109 @@
|
|
|
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, queued } = await fetchWithCache<TData>(url, fetchOptions);
|
|
56
|
+
if (queued) {
|
|
57
|
+
resolveMutation(mutationId, null);
|
|
58
|
+
setState({
|
|
59
|
+
data: null,
|
|
60
|
+
error: null,
|
|
61
|
+
isLoading: false,
|
|
62
|
+
isError: false,
|
|
63
|
+
isSuccess: false,
|
|
64
|
+
});
|
|
65
|
+
optionsRef.current.onSettled?.();
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const data: TData = await response.json();
|
|
69
|
+
resolveMutation(mutationId, data);
|
|
70
|
+
setState({
|
|
71
|
+
data,
|
|
72
|
+
error: null,
|
|
73
|
+
isLoading: false,
|
|
74
|
+
isError: false,
|
|
75
|
+
isSuccess: true,
|
|
76
|
+
});
|
|
77
|
+
optionsRef.current.onSuccess?.(data);
|
|
78
|
+
optionsRef.current.onSettled?.();
|
|
79
|
+
return data;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
82
|
+
rejectMutation(mutationId, error);
|
|
83
|
+
setState({
|
|
84
|
+
data: null,
|
|
85
|
+
error,
|
|
86
|
+
isLoading: false,
|
|
87
|
+
isError: true,
|
|
88
|
+
isSuccess: false,
|
|
89
|
+
});
|
|
90
|
+
optionsRef.current.onError?.(error);
|
|
91
|
+
optionsRef.current.onSettled?.();
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
[],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const reset = useCallback(() => {
|
|
99
|
+
setState({
|
|
100
|
+
data: null,
|
|
101
|
+
error: null,
|
|
102
|
+
isLoading: false,
|
|
103
|
+
isError: false,
|
|
104
|
+
isSuccess: false,
|
|
105
|
+
});
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
return { ...state, mutate, reset };
|
|
109
|
+
}
|
|
@@ -1,25 +1,34 @@
|
|
|
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
|
-
|
|
20
|
+
// Defer initial data load to avoid cascading render warning
|
|
21
|
+
queueMicrotask(() => refresh());
|
|
12
22
|
|
|
13
23
|
const onSync = (e) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
24
|
+
refresh();
|
|
25
|
+
setState((s) => ({
|
|
26
|
+
...s,
|
|
27
|
+
lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
|
|
28
|
+
}));
|
|
20
29
|
};
|
|
21
30
|
const onChange = () => {
|
|
22
|
-
|
|
31
|
+
refresh();
|
|
23
32
|
};
|
|
24
33
|
|
|
25
34
|
window.addEventListener("mutation-sync-complete", onSync);
|
|
@@ -28,7 +37,7 @@ export function useMutationQueue() {
|
|
|
28
37
|
window.removeEventListener("mutation-sync-complete", onSync);
|
|
29
38
|
window.removeEventListener("mutation-queue-changed", onChange);
|
|
30
39
|
};
|
|
31
|
-
}, []);
|
|
40
|
+
}, [refresh]);
|
|
32
41
|
|
|
33
42
|
return state;
|
|
34
43
|
}
|
|
@@ -1,25 +1,41 @@
|
|
|
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:
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
27
|
+
// Defer initial data load to avoid cascading render warning
|
|
28
|
+
queueMicrotask(() => refresh());
|
|
12
29
|
|
|
13
30
|
const onSync = (e: CustomEvent) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
31
|
+
refresh();
|
|
32
|
+
setState((s) => ({
|
|
33
|
+
...s,
|
|
34
|
+
lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
|
|
35
|
+
}));
|
|
20
36
|
};
|
|
21
37
|
const onChange = () => {
|
|
22
|
-
|
|
38
|
+
refresh();
|
|
23
39
|
};
|
|
24
40
|
|
|
25
41
|
window.addEventListener("mutation-sync-complete", onSync);
|
|
@@ -28,7 +44,7 @@ export function useMutationQueue() {
|
|
|
28
44
|
window.removeEventListener("mutation-sync-complete", onSync);
|
|
29
45
|
window.removeEventListener("mutation-queue-changed", onChange);
|
|
30
46
|
};
|
|
31
|
-
}, []);
|
|
47
|
+
}, [refresh]);
|
|
32
48
|
|
|
33
49
|
return state;
|
|
34
50
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { getMutationState, onMutationStateChange } from "../mutation-state.js";
|
|
3
|
+
|
|
4
|
+
function subscribeToMutations(cb) {
|
|
5
|
+
return onMutationStateChange(() => cb());
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function useMutationState(id) {
|
|
9
|
+
return useSyncExternalStore(
|
|
10
|
+
subscribeToMutations,
|
|
11
|
+
() => (id ? getMutationState(id) ?? null : null),
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import {
|
|
3
|
+
getMutationState,
|
|
4
|
+
onMutationStateChange,
|
|
5
|
+
} from "../mutation-state.ts";
|
|
6
|
+
import type { MutationState } from "../mutation-state.ts";
|
|
7
|
+
|
|
8
|
+
function subscribeToMutations(cb: () => void) {
|
|
9
|
+
return onMutationStateChange(() => cb());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hook that subscribes to a specific mutation's state changes.
|
|
14
|
+
* Useful for showing per-mutation loading spinners, error states, etc.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const mutation = useMutationState(mutationId);
|
|
18
|
+
* if (mutation?.status === "error") { ... }
|
|
19
|
+
*/
|
|
20
|
+
export function useMutationState(id: string | null): MutationState | null {
|
|
21
|
+
return useSyncExternalStore(
|
|
22
|
+
subscribeToMutations,
|
|
23
|
+
() => (id ? getMutationState(id) ?? null : null),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getPushSubscription,
|
|
7
7
|
} from "../push.js";
|
|
8
8
|
|
|
9
|
-
export function usePushSubscription(
|
|
9
|
+
export function usePushSubscription() {
|
|
10
10
|
const [state, setState] = useState({
|
|
11
11
|
subscribed: false,
|
|
12
12
|
subscription: null,
|
|
@@ -55,9 +55,9 @@ export function usePushSubscription(vapidPublicKey) {
|
|
|
55
55
|
}, []);
|
|
56
56
|
|
|
57
57
|
const subscribe = useCallback(async () => {
|
|
58
|
-
const sub = await subscribeToPush(
|
|
58
|
+
const sub = await subscribeToPush();
|
|
59
59
|
return sub !== null;
|
|
60
|
-
}, [
|
|
60
|
+
}, []);
|
|
61
61
|
|
|
62
62
|
const unsubscribe = useCallback(async () => {
|
|
63
63
|
await unsubscribeFromPush();
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getPushSubscription,
|
|
7
7
|
} from "../push.ts";
|
|
8
8
|
|
|
9
|
-
export function usePushSubscription(
|
|
9
|
+
export function usePushSubscription() {
|
|
10
10
|
const [state, setState] = useState({
|
|
11
11
|
subscribed: false,
|
|
12
12
|
subscription: null as PushSubscription | null,
|
|
@@ -55,9 +55,9 @@ export function usePushSubscription(vapidPublicKey: string) {
|
|
|
55
55
|
}, []);
|
|
56
56
|
|
|
57
57
|
const subscribe = useCallback(async () => {
|
|
58
|
-
const sub = await subscribeToPush(
|
|
58
|
+
const sub = await subscribeToPush();
|
|
59
59
|
return sub !== null;
|
|
60
|
-
}, [
|
|
60
|
+
}, []);
|
|
61
61
|
|
|
62
62
|
const unsubscribe = useCallback(async () => {
|
|
63
63
|
await unsubscribeFromPush();
|