@swoff/cli 0.3.6 → 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.
- package/README.md +43 -326
- package/bin/swoff +8 -14
- package/dist/__tests__/assemble-sw.test.js +6 -6
- package/dist/__tests__/assemble-sw.test.js.map +1 -1
- package/dist/__tests__/cli-integration.test.js +0 -1
- package/dist/__tests__/cli-integration.test.js.map +1 -1
- package/dist/__tests__/config-types.test.js +9 -5
- package/dist/__tests__/config-types.test.js.map +1 -1
- package/dist/__tests__/file-generators.test.js +12 -23
- package/dist/__tests__/file-generators.test.js.map +1 -1
- package/dist/__tests__/loader.test.js +0 -1
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/validator.test.js +50 -2
- package/dist/__tests__/validator.test.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/add.js +7 -6
- package/dist/lib/commands/add.js.map +1 -1
- package/dist/lib/commands/generate-guide.js +36 -9
- package/dist/lib/commands/generate-guide.js.map +1 -1
- package/dist/lib/commands/info.js +45 -8
- package/dist/lib/commands/info.js.map +1 -1
- package/dist/lib/commands/init.js +6 -2
- package/dist/lib/commands/init.js.map +1 -1
- package/dist/lib/config/validator.js +85 -4
- package/dist/lib/config/validator.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-fetch.js +3 -3
- package/dist/lib/generators/file-generators/auth-fetch.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-store.js +114 -5
- package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-user.js +2 -2
- package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
- package/dist/lib/generators/file-generators/cache.js +1 -3
- package/dist/lib/generators/file-generators/cache.js.map +1 -1
- package/dist/lib/generators/file-generators/client-injector.js +5 -17
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-wrapper.js +86 -20
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-hooks.js +28 -210
- package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
- package/dist/lib/generators/file-generators/gql-wrapper.js +152 -0
- package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -0
- package/dist/lib/generators/file-generators/guide-generator.js +357 -36
- package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-queue.js +130 -34
- 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 +143 -0
- package/dist/lib/generators/file-generators/push.js.map +1 -0
- package/dist/lib/generators/file-generators/quick-readme.js +71 -11
- package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
- package/dist/lib/generators/file-generators/server-push.js +136 -0
- package/dist/lib/generators/file-generators/server-push.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-generator-build.js +1 -1
- package/dist/lib/generators/file-generators/sw-injector.js +1 -1
- package/dist/lib/generators/file-generators/sw-push.js +67 -0
- package/dist/lib/generators/file-generators/sw-push.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-template.js +2 -0
- package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
- package/dist/lib/generators/file-generators/type-definitions.js +39 -11
- package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
- package/dist/lib/generators/sw-generator.js +8 -9
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js +6 -2
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/assemble-sw.js +11 -2
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/background-sync-handler.js +22 -8
- package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/config-header.js +1 -1
- package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
- package/dist/lib/generators/sw-sections/default-template.js +2 -30
- package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
- package/dist/lib/generators/sw-sections/fetch-handler.js +230 -120
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/install-handler.js +12 -10
- package/dist/lib/generators/sw-sections/install-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/tag-management.js +17 -5
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +16 -10
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +44 -4
- package/dist/lib/shared/config-types.js.map +1 -1
- package/package.json +3 -2
- package/templates/hooks/useAuth.jsx +30 -0
- package/templates/hooks/useAuth.tsx +30 -0
- package/templates/hooks/useBackgroundSync.jsx +31 -0
- package/templates/hooks/useBackgroundSync.tsx +31 -0
- package/templates/hooks/useCacheInvalidation.jsx +19 -0
- package/templates/hooks/useCacheInvalidation.tsx +19 -0
- package/templates/hooks/useCachedFetch.jsx +94 -0
- package/templates/hooks/useCachedFetch.tsx +112 -0
- package/templates/hooks/useMutation.jsx +76 -0
- package/templates/hooks/useMutation.tsx +97 -0
- package/templates/hooks/useMutationQueue.jsx +42 -0
- package/templates/hooks/useMutationQueue.tsx +49 -0
- package/templates/hooks/useMutationState.jsx +27 -0
- package/templates/hooks/useMutationState.tsx +41 -0
- package/templates/hooks/useNetworkStatus.jsx +19 -0
- package/templates/hooks/useNetworkStatus.tsx +19 -0
- package/templates/hooks/usePrefetch.jsx +10 -0
- package/templates/hooks/usePrefetch.tsx +25 -0
- package/templates/hooks/usePushSubscription.jsx +67 -0
- package/templates/hooks/usePushSubscription.tsx +67 -0
- package/templates/hooks/useSWUpdate.jsx +76 -0
- package/templates/hooks/useSWUpdate.tsx +76 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { getPendingCount, getQueueItems } from "../mutation-queue.js";
|
|
3
|
+
|
|
4
|
+
export function useMutationQueue() {
|
|
5
|
+
const [state, setState] = useState({
|
|
6
|
+
pending: 0,
|
|
7
|
+
items: [],
|
|
8
|
+
lastSync: null,
|
|
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
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
refresh();
|
|
21
|
+
|
|
22
|
+
const onSync = (e) => {
|
|
23
|
+
refresh();
|
|
24
|
+
setState((s) => ({
|
|
25
|
+
...s,
|
|
26
|
+
lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
|
|
27
|
+
}));
|
|
28
|
+
};
|
|
29
|
+
const onChange = () => {
|
|
30
|
+
refresh();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
window.addEventListener("mutation-sync-complete", onSync);
|
|
34
|
+
window.addEventListener("mutation-queue-changed", onChange);
|
|
35
|
+
return () => {
|
|
36
|
+
window.removeEventListener("mutation-sync-complete", onSync);
|
|
37
|
+
window.removeEventListener("mutation-queue-changed", onChange);
|
|
38
|
+
};
|
|
39
|
+
}, [refresh]);
|
|
40
|
+
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useMutationQueue() {
|
|
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
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
refresh();
|
|
28
|
+
|
|
29
|
+
const onSync = (e: CustomEvent) => {
|
|
30
|
+
refresh();
|
|
31
|
+
setState((s) => ({
|
|
32
|
+
...s,
|
|
33
|
+
lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
|
|
34
|
+
}));
|
|
35
|
+
};
|
|
36
|
+
const onChange = () => {
|
|
37
|
+
refresh();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
window.addEventListener("mutation-sync-complete", onSync);
|
|
41
|
+
window.addEventListener("mutation-queue-changed", onChange);
|
|
42
|
+
return () => {
|
|
43
|
+
window.removeEventListener("mutation-sync-complete", onSync);
|
|
44
|
+
window.removeEventListener("mutation-queue-changed", onChange);
|
|
45
|
+
};
|
|
46
|
+
}, [refresh]);
|
|
47
|
+
|
|
48
|
+
return state;
|
|
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,19 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export function useNetworkStatus() {
|
|
4
|
+
const [online, setOnline] = useState(navigator.onLine);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const onOnline = () => setOnline(true);
|
|
8
|
+
const onOffline = () => setOnline(false);
|
|
9
|
+
|
|
10
|
+
window.addEventListener("online", onOnline);
|
|
11
|
+
window.addEventListener("offline", onOffline);
|
|
12
|
+
return () => {
|
|
13
|
+
window.removeEventListener("online", onOnline);
|
|
14
|
+
window.removeEventListener("offline", onOffline);
|
|
15
|
+
};
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
return online;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export function useNetworkStatus() {
|
|
4
|
+
const [online, setOnline] = useState(navigator.onLine);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const onOnline = () => setOnline(true);
|
|
8
|
+
const onOffline = () => setOnline(false);
|
|
9
|
+
|
|
10
|
+
window.addEventListener("online", onOnline);
|
|
11
|
+
window.addEventListener("offline", onOffline);
|
|
12
|
+
return () => {
|
|
13
|
+
window.removeEventListener("online", onOnline);
|
|
14
|
+
window.removeEventListener("offline", onOffline);
|
|
15
|
+
};
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
return online;
|
|
19
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
subscribeToPush,
|
|
4
|
+
unsubscribeFromPush,
|
|
5
|
+
isSubscribed,
|
|
6
|
+
getPushSubscription,
|
|
7
|
+
} from "../push.js";
|
|
8
|
+
|
|
9
|
+
export function usePushSubscription(vapidPublicKey) {
|
|
10
|
+
const [state, setState] = useState({
|
|
11
|
+
subscribed: false,
|
|
12
|
+
subscription: null,
|
|
13
|
+
permission: Notification.permission,
|
|
14
|
+
loading: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
|
|
20
|
+
isSubscribed().then((subscribed) => {
|
|
21
|
+
if (cancelled) return;
|
|
22
|
+
if (subscribed) {
|
|
23
|
+
getPushSubscription().then((sub) => {
|
|
24
|
+
if (cancelled) return;
|
|
25
|
+
setState({ subscribed: true, subscription: sub, permission: Notification.permission, loading: false });
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
setState({ subscribed: false, subscription: null, permission: Notification.permission, loading: false });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const onSubChanged = (e) => {
|
|
33
|
+
if (cancelled) return;
|
|
34
|
+
if (!e.detail.subscribed) {
|
|
35
|
+
setState({ subscribed: false, subscription: null, permission: Notification.permission, loading: false });
|
|
36
|
+
} else {
|
|
37
|
+
getPushSubscription().then((sub) => {
|
|
38
|
+
if (cancelled) return;
|
|
39
|
+
setState({ subscribed: true, subscription: sub, permission: Notification.permission, loading: false });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const onPermissionChanged = (e) => {
|
|
44
|
+
if (cancelled) return;
|
|
45
|
+
setState((s) => ({ ...s, permission: e.detail.permission }));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
window.addEventListener("push-subscription-changed", onSubChanged);
|
|
49
|
+
window.addEventListener("push-permission-changed", onPermissionChanged);
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
window.removeEventListener("push-subscription-changed", onSubChanged);
|
|
53
|
+
window.removeEventListener("push-permission-changed", onPermissionChanged);
|
|
54
|
+
};
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const subscribe = useCallback(async () => {
|
|
58
|
+
const sub = await subscribeToPush(vapidPublicKey);
|
|
59
|
+
return sub !== null;
|
|
60
|
+
}, [vapidPublicKey]);
|
|
61
|
+
|
|
62
|
+
const unsubscribe = useCallback(async () => {
|
|
63
|
+
await unsubscribeFromPush();
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
return { ...state, subscribe, unsubscribe };
|
|
67
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
subscribeToPush,
|
|
4
|
+
unsubscribeFromPush,
|
|
5
|
+
isSubscribed,
|
|
6
|
+
getPushSubscription,
|
|
7
|
+
} from "../push.ts";
|
|
8
|
+
|
|
9
|
+
export function usePushSubscription(vapidPublicKey: string) {
|
|
10
|
+
const [state, setState] = useState({
|
|
11
|
+
subscribed: false,
|
|
12
|
+
subscription: null as PushSubscription | null,
|
|
13
|
+
permission: Notification.permission,
|
|
14
|
+
loading: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
|
|
20
|
+
isSubscribed().then((subscribed) => {
|
|
21
|
+
if (cancelled) return;
|
|
22
|
+
if (subscribed) {
|
|
23
|
+
getPushSubscription().then((sub) => {
|
|
24
|
+
if (cancelled) return;
|
|
25
|
+
setState({ subscribed: true, subscription: sub, permission: Notification.permission, loading: false });
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
setState({ subscribed: false, subscription: null, permission: Notification.permission, loading: false });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const onSubChanged = (e: CustomEvent) => {
|
|
33
|
+
if (cancelled) return;
|
|
34
|
+
if (!e.detail.subscribed) {
|
|
35
|
+
setState({ subscribed: false, subscription: null, permission: Notification.permission, loading: false });
|
|
36
|
+
} else {
|
|
37
|
+
getPushSubscription().then((sub) => {
|
|
38
|
+
if (cancelled) return;
|
|
39
|
+
setState({ subscribed: true, subscription: sub, permission: Notification.permission, loading: false });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const onPermissionChanged = (e: CustomEvent) => {
|
|
44
|
+
if (cancelled) return;
|
|
45
|
+
setState((s) => ({ ...s, permission: e.detail.permission }));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
window.addEventListener("push-subscription-changed", onSubChanged);
|
|
49
|
+
window.addEventListener("push-permission-changed", onPermissionChanged);
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
window.removeEventListener("push-subscription-changed", onSubChanged);
|
|
53
|
+
window.removeEventListener("push-permission-changed", onPermissionChanged);
|
|
54
|
+
};
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const subscribe = useCallback(async () => {
|
|
58
|
+
const sub = await subscribeToPush(vapidPublicKey);
|
|
59
|
+
return sub !== null;
|
|
60
|
+
}, [vapidPublicKey]);
|
|
61
|
+
|
|
62
|
+
const unsubscribe = useCallback(async () => {
|
|
63
|
+
await unsubscribeFromPush();
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
return { ...state, subscribe, unsubscribe };
|
|
67
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { handleUpdateApproved } from "../sw/injector.js";
|
|
3
|
+
|
|
4
|
+
export function useSWUpdate() {
|
|
5
|
+
const [state, setState] = useState({
|
|
6
|
+
updateStatus: "idle",
|
|
7
|
+
currentVersion: window.currentSWVersion || null,
|
|
8
|
+
availableVersion: null,
|
|
9
|
+
progress: 0,
|
|
10
|
+
forceUpdate: false,
|
|
11
|
+
error: null,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (sessionStorage.getItem("sw-dismissed-update") === "true") return;
|
|
16
|
+
|
|
17
|
+
const onAvailable = (e) =>
|
|
18
|
+
setState((s) => ({
|
|
19
|
+
...s,
|
|
20
|
+
updateStatus: "available",
|
|
21
|
+
availableVersion: e.detail.version,
|
|
22
|
+
forceUpdate: window.swUpdateRequired || false,
|
|
23
|
+
}));
|
|
24
|
+
const onProgress = (e) =>
|
|
25
|
+
setState((s) => ({ ...s, updateStatus: "downloading", progress: e.detail.percent }));
|
|
26
|
+
const onReady = () =>
|
|
27
|
+
setState((s) => ({ ...s, updateStatus: "idle", progress: 0 }));
|
|
28
|
+
const onError = () =>
|
|
29
|
+
setState((s) => ({ ...s, error: "SW registration failed" }));
|
|
30
|
+
|
|
31
|
+
window.addEventListener("sw-update-available", onAvailable);
|
|
32
|
+
window.addEventListener("sw-progress", onProgress);
|
|
33
|
+
window.addEventListener("sw-ready", onReady);
|
|
34
|
+
window.addEventListener("sw-error", onError);
|
|
35
|
+
return () => {
|
|
36
|
+
window.removeEventListener("sw-update-available", onAvailable);
|
|
37
|
+
window.removeEventListener("sw-progress", onProgress);
|
|
38
|
+
window.removeEventListener("sw-ready", onReady);
|
|
39
|
+
window.removeEventListener("sw-error", onError);
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const acceptUpdate = useCallback(async () => {
|
|
44
|
+
if (!state.availableVersion) return;
|
|
45
|
+
await handleUpdateApproved(state.availableVersion);
|
|
46
|
+
}, [state.availableVersion]);
|
|
47
|
+
|
|
48
|
+
const dismissUpdate = useCallback(() => {
|
|
49
|
+
sessionStorage.setItem("sw-dismissed-update", "true");
|
|
50
|
+
setState((s) => ({ ...s, updateStatus: "idle" }));
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
return { ...state, acceptUpdate, dismissUpdate };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function useSWProgress() {
|
|
57
|
+
const [state, setState] = useState({
|
|
58
|
+
status: "idle",
|
|
59
|
+
progress: 0,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const onProgress = (e) =>
|
|
64
|
+
setState({ status: "installing", progress: e.detail.percent });
|
|
65
|
+
const onReady = () => setState({ status: "idle", progress: 0 });
|
|
66
|
+
|
|
67
|
+
window.addEventListener("sw-progress", onProgress);
|
|
68
|
+
window.addEventListener("sw-ready", onReady);
|
|
69
|
+
return () => {
|
|
70
|
+
window.removeEventListener("sw-progress", onProgress);
|
|
71
|
+
window.removeEventListener("sw-ready", onReady);
|
|
72
|
+
};
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
return state;
|
|
76
|
+
}
|