@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.
- package/README.md +28 -32
- package/dist/index.js +2 -13
- package/dist/index.js.map +1 -1
- package/dist/lib/cli/help.js +3 -8
- package/dist/lib/cli/help.js.map +1 -1
- package/dist/lib/commands/add.js +29 -87
- package/dist/lib/commands/add.js.map +1 -1
- package/dist/lib/commands/generate-guide.js +16 -30
- package/dist/lib/commands/generate-guide.js.map +1 -1
- package/dist/lib/commands/generate.js +31 -6
- package/dist/lib/commands/generate.js.map +1 -1
- package/dist/lib/commands/init.js +188 -11
- package/dist/lib/commands/init.js.map +1 -1
- package/dist/lib/commands/validate.js +2 -4
- package/dist/lib/commands/validate.js.map +1 -1
- package/dist/lib/config/loader.js +34 -25
- package/dist/lib/config/loader.js.map +1 -1
- package/dist/lib/config/validator.js +202 -96
- package/dist/lib/config/validator.js.map +1 -1
- package/dist/lib/generators/file-generators/api-config.js +1 -1
- package/dist/lib/generators/file-generators/api-config.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-adapter.js +13 -0
- package/dist/lib/generators/file-generators/auth-adapter.js.map +1 -0
- package/dist/lib/generators/file-generators/auth-check.js +40 -0
- package/dist/lib/generators/file-generators/auth-check.js.map +1 -0
- package/dist/lib/generators/file-generators/auth-store.js +2 -5
- package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
- package/dist/lib/generators/file-generators/background-sync.js +1 -1
- package/dist/lib/generators/file-generators/background-sync.js.map +1 -1
- package/dist/lib/generators/file-generators/cache.js +2 -1
- package/dist/lib/generators/file-generators/cache.js.map +1 -1
- package/dist/lib/generators/file-generators/client-injector-bundle.js +15 -0
- package/dist/lib/generators/file-generators/client-injector-bundle.js.map +1 -0
- package/dist/lib/generators/file-generators/client-injector.js +2 -1
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/connectivity-manager.js +12 -0
- package/dist/lib/generators/file-generators/connectivity-manager.js.map +1 -0
- package/dist/lib/generators/file-generators/connectivity.js +12 -0
- package/dist/lib/generators/file-generators/connectivity.js.map +1 -0
- package/dist/lib/generators/file-generators/context.js +7 -0
- package/dist/lib/generators/file-generators/context.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-wrapper.js +1 -1
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-framework-adapters.js +29 -20
- package/dist/lib/generators/file-generators/generate-framework-adapters.js.map +1 -1
- package/dist/lib/generators/file-generators/gql-wrapper.js +2 -2
- package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/guide-generator.js +51 -1
- package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
- package/dist/lib/generators/file-generators/invalidation-tags.js +2 -3
- package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-queue.js +2 -2
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-state.js +1 -1
- package/dist/lib/generators/file-generators/mutation-state.js.map +1 -1
- package/dist/lib/generators/file-generators/notification-generator.js +6 -0
- package/dist/lib/generators/file-generators/notification-generator.js.map +1 -0
- package/dist/lib/generators/file-generators/open-db.js +6 -0
- package/dist/lib/generators/file-generators/open-db.js.map +1 -0
- package/dist/lib/generators/file-generators/push.js +1 -2
- package/dist/lib/generators/file-generators/push.js.map +1 -1
- package/dist/lib/generators/file-generators/pwa-install.js +2 -6
- package/dist/lib/generators/file-generators/pwa-install.js.map +1 -1
- package/dist/lib/generators/file-generators/server-push.js +1 -1
- package/dist/lib/generators/file-generators/server-push.js.map +1 -1
- package/dist/lib/generators/file-generators/storage-generator.js +6 -0
- package/dist/lib/generators/file-generators/storage-generator.js.map +1 -0
- package/dist/lib/generators/file-generators/storage-notify.js +6 -0
- package/dist/lib/generators/file-generators/storage-notify.js.map +1 -0
- package/dist/lib/generators/file-generators/storage.js +6 -0
- package/dist/lib/generators/file-generators/storage.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-generator-build.js +96 -45
- package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-injector.js +0 -4
- package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-template.js +8 -9
- package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-version-gen.js +32 -0
- package/dist/lib/generators/file-generators/sw-version-gen.js.map +1 -0
- package/dist/lib/generators/file-generators/swoff-api-bundle.js +51 -0
- package/dist/lib/generators/file-generators/swoff-api-bundle.js.map +1 -0
- package/dist/lib/generators/sw-build-utils.js +69 -0
- package/dist/lib/generators/sw-build-utils.js.map +1 -0
- package/dist/lib/generators/sw-generator.js +23 -34
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js +74 -23
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/assemble-sw.js +15 -94
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/background-sync-handler.js +74 -72
- package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/batch-refresh-queue.js +102 -0
- package/dist/lib/generators/sw-sections/batch-refresh-queue.js.map +1 -0
- package/dist/lib/generators/sw-sections/config-header.js +2 -3
- package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
- package/dist/lib/generators/sw-sections/default-template.js +59 -6
- package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
- package/dist/lib/generators/sw-sections/fetch-handler.js +621 -495
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/install-handler.js +40 -23
- package/dist/lib/generators/sw-sections/install-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/message-handler.js +25 -8
- package/dist/lib/generators/sw-sections/message-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/server-push-handler.js +10 -14
- package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/shared.js +68 -0
- package/dist/lib/generators/sw-sections/shared.js.map +1 -0
- package/dist/lib/generators/sw-sections/sw-push.js +2 -6
- package/dist/lib/generators/sw-sections/sw-push.js.map +1 -1
- package/dist/lib/generators/sw-sections/tag-management.js +55 -86
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +183 -59
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +67 -63
- package/dist/lib/shared/config-types.js.map +1 -1
- package/dist/lib/shared/feature-registry.js +198 -0
- package/dist/lib/shared/feature-registry.js.map +1 -0
- package/dist/lib/utils/detect-framework.js +52 -9
- package/dist/lib/utils/detect-framework.js.map +1 -1
- package/dist/runtime/api-config.js +2 -2
- package/dist/runtime/api-config.js.map +1 -1
- package/dist/runtime/auth-adapter.js +179 -0
- package/dist/runtime/auth-adapter.js.map +1 -0
- package/dist/runtime/auth-state.js +16 -6
- package/dist/runtime/auth-state.js.map +1 -1
- package/dist/runtime/auth-store.js +219 -213
- package/dist/runtime/auth-store.js.map +1 -1
- package/dist/runtime/auth-user.js +5 -19
- package/dist/runtime/auth-user.js.map +1 -1
- package/dist/runtime/background-sync.js +5 -2
- package/dist/runtime/background-sync.js.map +1 -1
- package/dist/runtime/cache.js +91 -3
- package/dist/runtime/cache.js.map +1 -1
- package/dist/runtime/client-injector-bundle.js +314 -0
- package/dist/runtime/client-injector-bundle.js.map +1 -0
- package/dist/runtime/client-injector.js +173 -30
- package/dist/runtime/client-injector.js.map +1 -1
- package/dist/runtime/connectivity-manager.js +83 -0
- package/dist/runtime/connectivity-manager.js.map +1 -0
- package/dist/runtime/connectivity.js +83 -0
- package/dist/runtime/connectivity.js.map +1 -0
- package/dist/runtime/fetch-wrapper.js +26 -59
- package/dist/runtime/fetch-wrapper.js.map +1 -1
- package/dist/runtime/gql-wrapper.js +15 -9
- package/dist/runtime/gql-wrapper.js.map +1 -1
- package/dist/runtime/invalidation-tags.js +12 -89
- package/dist/runtime/invalidation-tags.js.map +1 -1
- package/dist/runtime/mutation-queue.js +83 -31
- package/dist/runtime/mutation-queue.js.map +1 -1
- package/dist/runtime/mutation-state.js +3 -3
- package/dist/runtime/mutation-state.js.map +1 -1
- package/dist/runtime/notification.js +65 -0
- package/dist/runtime/notification.js.map +1 -0
- package/dist/runtime/open-db.js +41 -0
- package/dist/runtime/open-db.js.map +1 -0
- package/dist/runtime/push.js +15 -27
- package/dist/runtime/push.js.map +1 -1
- package/dist/runtime/pwa-injector.js +3 -32
- package/dist/runtime/pwa-injector.js.map +1 -1
- package/dist/runtime/pwa-prompt.js +35 -3
- package/dist/runtime/pwa-prompt.js.map +1 -1
- package/dist/runtime/reset.js +39 -42
- package/dist/runtime/reset.js.map +1 -1
- package/dist/runtime/server-push.js +23 -13
- package/dist/runtime/server-push.js.map +1 -1
- package/dist/runtime/storage.js +32 -0
- package/dist/runtime/storage.js.map +1 -0
- package/dist/runtime/sw-injector.js +19 -178
- package/dist/runtime/sw-injector.js.map +1 -1
- package/dist/runtime/sw-version.js +13 -0
- package/dist/runtime/sw-version.js.map +1 -0
- package/dist/runtime/swoff-api-bundle.js +1491 -0
- package/dist/runtime/swoff-api-bundle.js.map +1 -0
- package/dist/runtime/type-definitions.js +8 -7
- package/dist/runtime/type-definitions.js.map +1 -1
- package/dist/runtime/utils.js +4 -0
- package/dist/runtime/utils.js.map +1 -1
- package/dist/swoff-nkdhhmw8. +18 -0
- package/dist/swoff.js +5 -0
- package/package.json +1 -1
- package/templates/react/useAuth.jsx +7 -4
- package/templates/react/useAuth.tsx +14 -14
- package/templates/react/useBackgroundSync.jsx +5 -4
- package/templates/react/useBackgroundSync.tsx +5 -4
- package/templates/react/useCachedFetch.jsx +8 -0
- package/templates/react/useCachedFetch.tsx +8 -0
- package/templates/react/useMutation.jsx +60 -56
- package/templates/react/useMutation.tsx +65 -64
- package/templates/react/useMutationQueue.jsx +5 -1
- package/templates/react/useMutationQueue.tsx +8 -2
- package/templates/react/useMutationState.jsx +3 -4
- package/templates/react/useMutationState.tsx +4 -8
- package/templates/react/useNetworkStatus.jsx +57 -17
- package/templates/react/useNetworkStatus.tsx +42 -20
- package/templates/react/useOfflineAnalytics.jsx +42 -0
- package/templates/react/useOfflineAnalytics.tsx +50 -0
- package/templates/react/usePwaInstall.jsx +26 -0
- package/templates/react/usePwaInstall.tsx +26 -0
- package/templates/react/useSWUpdate.jsx +7 -52
- package/templates/react/useSWUpdate.tsx +12 -51
- package/templates/react/useStorageEstimate.jsx +56 -0
- package/templates/react/useStorageEstimate.tsx +67 -0
- package/dist/__tests__/assemble-sw.test.js +0 -145
- package/dist/__tests__/assemble-sw.test.js.map +0 -1
- package/dist/__tests__/cli-integration.test.js +0 -231
- package/dist/__tests__/cli-integration.test.js.map +0 -1
- package/dist/__tests__/config-types.test.js +0 -124
- package/dist/__tests__/config-types.test.js.map +0 -1
- package/dist/__tests__/detect-language.test.js +0 -51
- package/dist/__tests__/detect-language.test.js.map +0 -1
- package/dist/__tests__/file-generators.test.js +0 -228
- package/dist/__tests__/file-generators.test.js.map +0 -1
- package/dist/__tests__/glob-matcher.test.js +0 -86
- package/dist/__tests__/glob-matcher.test.js.map +0 -1
- package/dist/__tests__/invalidation-tags.test.js +0 -283
- package/dist/__tests__/invalidation-tags.test.js.map +0 -1
- package/dist/__tests__/loader.test.js +0 -77
- package/dist/__tests__/loader.test.js.map +0 -1
- package/dist/__tests__/validator.test.js +0 -340
- package/dist/__tests__/validator.test.js.map +0 -1
- package/templates/react/useCacheInvalidation.jsx +0 -19
- package/templates/react/useCacheInvalidation.tsx +0 -52
- package/templates/react/useIsFetching.jsx +0 -32
- package/templates/react/useIsFetching.tsx +0 -32
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
trackMutation,
|
|
5
5
|
resolveMutation,
|
|
6
6
|
rejectMutation,
|
|
7
|
-
} from "../
|
|
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 [
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
82
|
+
): Promise<MutateResult<TData>> => {
|
|
81
83
|
const key = optionsRef.current.mutationKey;
|
|
82
|
-
if (key && inFlightKeys.current.has(key)) return
|
|
84
|
+
if (key && inFlightKeys.current.has(key)) return { status: "skipped" };
|
|
83
85
|
if (key) inFlightKeys.current.add(key);
|
|
84
86
|
|
|
85
|
-
let retriesLeft =
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
101
|
+
const attempt = async (): Promise<MutateResult<TData>> => {
|
|
103
102
|
try {
|
|
104
|
-
const
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
125
|
+
return r;
|
|
119
126
|
}
|
|
120
127
|
const data: TData = await response.json();
|
|
121
128
|
resolveMutation(mid, data);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
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 {
|
|
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 {
|
|
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 & {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
26
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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) => ({
|
|
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) => ({
|
|
80
|
+
setState((s) => ({
|
|
81
|
+
...s,
|
|
82
|
+
effectiveType: connection.effectiveType,
|
|
83
|
+
downlink: connection.downlink,
|
|
84
|
+
}));
|
|
43
85
|
};
|
|
44
86
|
|
|
45
|
-
window.addEventListener(
|
|
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(
|
|
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:
|
|
28
|
-
downlink:
|
|
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
|
-
|
|
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:
|
|
50
|
+
online: false,
|
|
51
|
+
wasOffline: true,
|
|
37
52
|
lastChangedAt: Date.now(),
|
|
38
53
|
}));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
+
}
|