@swoff/cli 0.4.3 → 0.4.5

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 (117) hide show
  1. package/dist/lib/commands/generate-guide.js +16 -16
  2. package/dist/lib/commands/generate-guide.js.map +1 -1
  3. package/dist/lib/commands/init.js +127 -17
  4. package/dist/lib/commands/init.js.map +1 -1
  5. package/dist/lib/config/validator.js +1 -1
  6. package/dist/lib/config/validator.js.map +1 -1
  7. package/dist/lib/generators/file-generators/client-injector-bundle.js +2 -1
  8. package/dist/lib/generators/file-generators/client-injector-bundle.js.map +1 -1
  9. package/dist/lib/generators/file-generators/client-injector.js +2 -1
  10. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  11. package/dist/lib/generators/file-generators/context.js +1 -1
  12. package/dist/lib/generators/file-generators/context.js.map +1 -1
  13. package/dist/lib/generators/file-generators/generate-framework-adapters.js +30 -23
  14. package/dist/lib/generators/file-generators/generate-framework-adapters.js.map +1 -1
  15. package/dist/lib/generators/sw-sections/background-precache.js +8 -1
  16. package/dist/lib/generators/sw-sections/background-precache.js.map +1 -1
  17. package/dist/lib/generators/sw-sections/batch-refresh-queue.js +2 -5
  18. package/dist/lib/generators/sw-sections/batch-refresh-queue.js.map +1 -1
  19. package/dist/lib/generators/sw-sections/fetch-handler.js +29 -7
  20. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  21. package/dist/lib/generators/swoff-files-generator.js +2 -1
  22. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  23. package/dist/lib/shared/config-types.js +1 -0
  24. package/dist/lib/shared/config-types.js.map +1 -1
  25. package/dist/lib/utils/detect-framework.js +4 -1
  26. package/dist/lib/utils/detect-framework.js.map +1 -1
  27. package/dist/runtime/client-injector-bundle.js +2 -2
  28. package/dist/runtime/client-injector-bundle.js.map +1 -1
  29. package/dist/runtime/client-injector.js +3 -2
  30. package/dist/runtime/client-injector.js.map +1 -1
  31. package/dist/runtime/gql-wrapper.js +3 -1
  32. package/dist/runtime/gql-wrapper.js.map +1 -1
  33. package/dist/runtime/swoff-api-bundle.js +2 -1
  34. package/dist/runtime/swoff-api-bundle.js.map +1 -1
  35. package/package.json +1 -1
  36. package/templates/react/{useOfflineAnalytics.jsx → useSwoffAnalytics.jsx} +3 -3
  37. package/templates/react/{useOfflineAnalytics.tsx → useSwoffAnalytics.tsx} +3 -3
  38. package/templates/react/{useAuth.jsx → useSwoffAuth.jsx} +2 -2
  39. package/templates/react/{useAuth.tsx → useSwoffAuth.tsx} +2 -2
  40. package/templates/react/{useCachedFetch.jsx → useSwoffFetch.jsx} +6 -6
  41. package/templates/react/{useCachedFetch.tsx → useSwoffFetch.tsx} +7 -7
  42. package/templates/react/{useMutation.jsx → useSwoffMutation.jsx} +3 -3
  43. package/templates/react/{useMutation.tsx → useSwoffMutation.tsx} +3 -3
  44. package/templates/react/{useMutationState.jsx → useSwoffMutationState.jsx} +1 -1
  45. package/templates/react/{useMutationState.tsx → useSwoffMutationState.tsx} +2 -2
  46. package/templates/react/{useNetworkStatus.jsx → useSwoffNetwork.jsx} +2 -2
  47. package/templates/react/{useNetworkStatus.tsx → useSwoffNetwork.tsx} +2 -2
  48. package/templates/react/{usePrecacheProgress.jsx → useSwoffPrecache.jsx} +4 -7
  49. package/templates/react/{usePrecacheProgress.tsx → useSwoffPrecache.tsx} +4 -7
  50. package/templates/react/{usePrefetch.jsx → useSwoffPrefetch.jsx} +2 -2
  51. package/templates/react/{usePrefetch.tsx → useSwoffPrefetch.tsx} +2 -2
  52. package/templates/react/{usePushSubscription.jsx → useSwoffPush.jsx} +1 -1
  53. package/templates/react/{usePushSubscription.tsx → useSwoffPush.tsx} +1 -1
  54. package/templates/react/{usePwaInstall.jsx → useSwoffPwa.jsx} +1 -1
  55. package/templates/react/{usePwaInstall.tsx → useSwoffPwa.tsx} +1 -1
  56. package/templates/react/{useMutationQueue.jsx → useSwoffQueue.jsx} +2 -2
  57. package/templates/react/{useMutationQueue.tsx → useSwoffQueue.tsx} +2 -2
  58. package/templates/react/{useStorageEstimate.jsx → useSwoffStorage.jsx} +2 -2
  59. package/templates/react/{useStorageEstimate.tsx → useSwoffStorage.tsx} +2 -2
  60. package/templates/react/{useBackgroundSync.jsx → useSwoffSync.jsx} +1 -1
  61. package/templates/react/{useBackgroundSync.tsx → useSwoffSync.tsx} +1 -1
  62. package/templates/svelte/useSwoffAnalytics.js +31 -0
  63. package/templates/svelte/useSwoffAnalytics.ts +37 -0
  64. package/templates/svelte/useSwoffAuth.js +95 -0
  65. package/templates/svelte/useSwoffAuth.ts +96 -0
  66. package/templates/svelte/useSwoffFetch.js +97 -0
  67. package/templates/svelte/useSwoffFetch.ts +112 -0
  68. package/templates/svelte/useSwoffMutation.js +110 -0
  69. package/templates/svelte/useSwoffMutation.ts +141 -0
  70. package/templates/svelte/useSwoffMutationState.js +27 -0
  71. package/templates/svelte/useSwoffMutationState.ts +28 -0
  72. package/templates/svelte/useSwoffNetwork.js +75 -0
  73. package/templates/svelte/useSwoffNetwork.ts +75 -0
  74. package/templates/svelte/useSwoffPrecache.js +19 -0
  75. package/templates/svelte/useSwoffPrecache.ts +19 -0
  76. package/templates/svelte/useSwoffPrefetch.js +19 -0
  77. package/templates/svelte/useSwoffPrefetch.ts +20 -0
  78. package/templates/svelte/useSwoffPush.js +82 -0
  79. package/templates/svelte/useSwoffPush.ts +82 -0
  80. package/templates/svelte/useSwoffPwa.js +27 -0
  81. package/templates/svelte/useSwoffPwa.ts +27 -0
  82. package/templates/svelte/useSwoffQueue.js +57 -0
  83. package/templates/svelte/useSwoffQueue.ts +67 -0
  84. package/templates/svelte/useSwoffReset.js +21 -0
  85. package/templates/svelte/useSwoffReset.ts +32 -0
  86. package/templates/svelte/useSwoffStorage.js +47 -0
  87. package/templates/svelte/useSwoffStorage.ts +47 -0
  88. package/templates/svelte/useSwoffSync.js +31 -0
  89. package/templates/svelte/useSwoffSync.ts +31 -0
  90. package/templates/vue/useSwoffAnalytics.js +46 -0
  91. package/templates/vue/useSwoffAnalytics.ts +54 -0
  92. package/templates/vue/useSwoffAuth.js +106 -0
  93. package/templates/vue/useSwoffAuth.ts +118 -0
  94. package/templates/vue/useSwoffFetch.js +127 -0
  95. package/templates/vue/useSwoffFetch.ts +143 -0
  96. package/templates/vue/useSwoffMutation.js +124 -0
  97. package/templates/vue/useSwoffMutation.ts +155 -0
  98. package/templates/vue/useSwoffMutationState.js +35 -0
  99. package/templates/vue/useSwoffMutationState.ts +36 -0
  100. package/templates/vue/useSwoffNetwork.js +86 -0
  101. package/templates/vue/useSwoffNetwork.ts +89 -0
  102. package/templates/vue/useSwoffPrecache.js +19 -0
  103. package/templates/vue/useSwoffPrecache.ts +19 -0
  104. package/templates/vue/useSwoffPrefetch.js +33 -0
  105. package/templates/vue/useSwoffPrefetch.ts +41 -0
  106. package/templates/vue/useSwoffPush.js +81 -0
  107. package/templates/vue/useSwoffPush.ts +81 -0
  108. package/templates/vue/useSwoffPwa.js +27 -0
  109. package/templates/vue/useSwoffPwa.ts +27 -0
  110. package/templates/vue/useSwoffQueue.js +70 -0
  111. package/templates/vue/useSwoffQueue.ts +82 -0
  112. package/templates/vue/useSwoffReset.js +32 -0
  113. package/templates/vue/useSwoffReset.ts +39 -0
  114. package/templates/vue/useSwoffStorage.js +56 -0
  115. package/templates/vue/useSwoffStorage.ts +57 -0
  116. package/templates/vue/useSwoffSync.js +30 -0
  117. package/templates/vue/useSwoffSync.ts +30 -0
@@ -0,0 +1,124 @@
1
+ import { ref } from "vue";
2
+ import { fetchWithCache } from "../fetch/core.js";
3
+ import {
4
+ trackMutation,
5
+ resolveMutation,
6
+ rejectMutation,
7
+ } from "../mutation/state.js";
8
+
9
+ /**
10
+ * Composable for mutations (POST, PUT, PATCH, DELETE) with auto-invalidation, offline queuing,
11
+ * optimistic updates, and dual-level callbacks.
12
+ *
13
+ * Usage:
14
+ * const { mutate, isLoading, error, data } = useSwoffMutation("/api/todos", {
15
+ * method: "POST",
16
+ * onSuccess: (data) => navigate(`/item/${data.id}`),
17
+ * });
18
+ *
19
+ * // Check result status after mutate:
20
+ * const result = await mutate(body);
21
+ * if (result.status === "success") navigate(`/item/${result.data.id}`);
22
+ */
23
+ export function useSwoffMutation(url, options = {}) {
24
+ const lastResult = ref(null);
25
+ const isLoading = ref(false);
26
+ const mutationId = ref(null);
27
+
28
+ const inFlightKeys = new Set();
29
+
30
+ async function mutate(body, callbacks) {
31
+ const key = options.mutationKey;
32
+ if (key && inFlightKeys.has(key)) return { status: "skipped" };
33
+ if (key) inFlightKeys.add(key);
34
+
35
+ let retriesLeft =
36
+ options.retry === true
37
+ ? Infinity
38
+ : typeof options.retry === "number"
39
+ ? options.retry
40
+ : 0;
41
+
42
+ const mid = "mut-" + crypto.randomUUID();
43
+ mutationId.value = mid;
44
+ trackMutation(mid, "pending");
45
+ isLoading.value = true;
46
+
47
+ options.onMutate?.();
48
+
49
+ const attempt = async () => {
50
+ try {
51
+ const {
52
+ mutationKey,
53
+ onMutate,
54
+ onSuccess,
55
+ onError,
56
+ onSettled,
57
+ retry,
58
+ ...fetchOnlyOptions
59
+ } = options;
60
+ const fetchOptions = { ...fetchOnlyOptions, body };
61
+ const { response, queued } = await fetchWithCache(url, fetchOptions);
62
+ if (queued) {
63
+ resolveMutation(mid, null);
64
+ isLoading.value = false;
65
+ const r = { status: "queued" };
66
+ lastResult.value = r;
67
+ options.onSettled?.();
68
+ callbacks?.onSettled?.();
69
+ if (key) inFlightKeys.delete(key);
70
+ return r;
71
+ }
72
+ const data = await response.json();
73
+ resolveMutation(mid, data);
74
+ isLoading.value = false;
75
+ const r = { status: "success", data };
76
+ lastResult.value = r;
77
+ options.onSuccess?.(data);
78
+ callbacks?.onSuccess?.(data);
79
+ options.onSettled?.();
80
+ callbacks?.onSettled?.();
81
+ if (key) inFlightKeys.delete(key);
82
+ return r;
83
+ } catch (err) {
84
+ if (retriesLeft > 0) {
85
+ retriesLeft--;
86
+ await new Promise((r) => setTimeout(r, 1000));
87
+ return attempt();
88
+ }
89
+ const error = err instanceof Error ? err : new Error(String(err));
90
+ rejectMutation(mid, error);
91
+ isLoading.value = false;
92
+ const r = { status: "error", error };
93
+ lastResult.value = r;
94
+ options.onError?.(error);
95
+ callbacks?.onError?.(error);
96
+ options.onSettled?.();
97
+ callbacks?.onSettled?.();
98
+ if (key) inFlightKeys.delete(key);
99
+ return r;
100
+ }
101
+ };
102
+
103
+ return attempt();
104
+ }
105
+
106
+ function reset() {
107
+ lastResult.value = null;
108
+ isLoading.value = false;
109
+ }
110
+
111
+ return {
112
+ mutate,
113
+ isLoading,
114
+ isIdle: lastResult.value === null,
115
+ isQueued: lastResult.value?.status === "queued",
116
+ isSuccess: lastResult.value?.status === "success",
117
+ isError: lastResult.value?.status === "error",
118
+ data: lastResult.value?.status === "success" ? (lastResult.value.data ?? null) : null,
119
+ error: lastResult.value?.status === "error" ? (lastResult.value.error ?? null) : null,
120
+ lastResult,
121
+ reset,
122
+ mutationId,
123
+ };
124
+ }
@@ -0,0 +1,155 @@
1
+ import { ref } from "vue";
2
+ import { fetchWithCache } from "../fetch/core";
3
+ import {
4
+ trackMutation,
5
+ resolveMutation,
6
+ rejectMutation,
7
+ } from "../mutation/state";
8
+ import type { FetchWithCacheOptions } from "../fetch/core";
9
+
10
+ export interface MutateCallbacks<TData> {
11
+ onSuccess?: (data: TData) => void;
12
+ onError?: (error: Error) => void;
13
+ onSettled?: () => void;
14
+ }
15
+
16
+ export interface UseMutationOptions<TData> extends FetchWithCacheOptions {
17
+ onMutate?: () => void;
18
+ onSuccess?: (data: TData) => void;
19
+ onError?: (error: Error) => void;
20
+ onSettled?: () => void;
21
+ mutationKey?: string;
22
+ retry?: number | boolean;
23
+ }
24
+
25
+ export type MutateResult<T> =
26
+ | { status: "skipped" }
27
+ | { status: "queued" }
28
+ | { status: "success"; data: T }
29
+ | { status: "error"; error: Error };
30
+
31
+ /**
32
+ * Composable for mutations (POST, PUT, PATCH, DELETE) with auto-invalidation, offline queuing,
33
+ * optimistic updates, and dual-level callbacks.
34
+ *
35
+ * Usage:
36
+ * const { mutate, isLoading, error, data } = useSwoffMutation("/api/todos", {
37
+ * method: "POST",
38
+ * onSuccess: (data) => navigate(`/item/${data.id}`),
39
+ * });
40
+ *
41
+ * // Check result status after mutate:
42
+ * const result = await mutate(body);
43
+ * if (result.status === "success") navigate(`/item/${result.data.id}`);
44
+ */
45
+ export function useSwoffMutation<TData = unknown>(
46
+ url: string,
47
+ options: UseMutationOptions<TData> = {},
48
+ ) {
49
+ const lastResult = ref<MutateResult<TData> | null>(null);
50
+ const isLoading = ref(false);
51
+ const mutationId = ref<string | null>(null);
52
+
53
+ const inFlightKeys = new Set<string>();
54
+
55
+ async function mutate(
56
+ body?: unknown,
57
+ callbacks?: MutateCallbacks<TData>,
58
+ ): Promise<MutateResult<TData>> {
59
+ const key = options.mutationKey;
60
+ if (key && inFlightKeys.has(key)) return { status: "skipped" };
61
+ if (key) inFlightKeys.add(key);
62
+
63
+ let retriesLeft: number =
64
+ options.retry === true
65
+ ? Infinity
66
+ : typeof options.retry === "number"
67
+ ? options.retry
68
+ : 0;
69
+
70
+ const mid = "mut-" + crypto.randomUUID();
71
+ mutationId.value = mid;
72
+ trackMutation(mid, "pending");
73
+ isLoading.value = true;
74
+
75
+ options.onMutate?.();
76
+
77
+ const attempt = async (): Promise<MutateResult<TData>> => {
78
+ try {
79
+ const {
80
+ mutationKey,
81
+ onMutate,
82
+ onSuccess,
83
+ onError,
84
+ onSettled,
85
+ retry,
86
+ ...fetchOnlyOptions
87
+ } = options;
88
+ const fetchOptions = { ...fetchOnlyOptions, body };
89
+ const { response, queued } = await fetchWithCache<TData>(
90
+ url,
91
+ fetchOptions as FetchWithCacheOptions,
92
+ );
93
+ if (queued) {
94
+ resolveMutation(mid, null);
95
+ isLoading.value = false;
96
+ const r: MutateResult<TData> = { status: "queued" };
97
+ lastResult.value = r;
98
+ options.onSettled?.();
99
+ callbacks?.onSettled?.();
100
+ if (key) inFlightKeys.delete(key);
101
+ return r;
102
+ }
103
+ const data: TData = await response.json();
104
+ resolveMutation(mid, data);
105
+ isLoading.value = false;
106
+ const r: MutateResult<TData> = { status: "success", data };
107
+ lastResult.value = r;
108
+ options.onSuccess?.(data);
109
+ callbacks?.onSuccess?.(data);
110
+ options.onSettled?.();
111
+ callbacks?.onSettled?.();
112
+ if (key) inFlightKeys.delete(key);
113
+ return r;
114
+ } catch (err) {
115
+ if (retriesLeft > 0) {
116
+ retriesLeft--;
117
+ await new Promise((r) => setTimeout(r, 1000));
118
+ return attempt();
119
+ }
120
+ const error = err instanceof Error ? err : new Error(String(err));
121
+ rejectMutation(mid, error);
122
+ isLoading.value = false;
123
+ const r: MutateResult<TData> = { status: "error", error };
124
+ lastResult.value = r;
125
+ options.onError?.(error);
126
+ callbacks?.onError?.(error);
127
+ options.onSettled?.();
128
+ callbacks?.onSettled?.();
129
+ if (key) inFlightKeys.delete(key);
130
+ return r;
131
+ }
132
+ };
133
+
134
+ return attempt();
135
+ }
136
+
137
+ function reset() {
138
+ lastResult.value = null;
139
+ isLoading.value = false;
140
+ }
141
+
142
+ return {
143
+ mutate,
144
+ isLoading,
145
+ isIdle: lastResult.value === null,
146
+ isQueued: lastResult.value?.status === "queued",
147
+ isSuccess: lastResult.value?.status === "success",
148
+ isError: lastResult.value?.status === "error",
149
+ data: lastResult.value?.status === "success" ? (lastResult.value.data ?? null) : null,
150
+ error: lastResult.value?.status === "error" ? (lastResult.value.error ?? null) : null,
151
+ lastResult,
152
+ reset,
153
+ mutationId,
154
+ };
155
+ }
@@ -0,0 +1,35 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+ import { getMutationState, onMutationStateChange } from "../mutation/state.js";
3
+
4
+ /**
5
+ * Composable that subscribes to a specific mutation's state changes.
6
+ * Useful for showing per-mutation loading spinners, error states, etc.
7
+ *
8
+ * Usage:
9
+ * const mutation = useSwoffMutationState(mutationId);
10
+ * <div v-if="mutation?.status === 'error'">{{ mutation?.error?.message }}</div>
11
+ */
12
+ export function useSwoffMutationState(id) {
13
+ const state = ref(null);
14
+
15
+ function update() {
16
+ state.value = id ? (getMutationState(id) ?? null) : null;
17
+ }
18
+
19
+ onMounted(update);
20
+
21
+ let cleanup;
22
+
23
+ onMounted(() => {
24
+ const unsub = onMutationStateChange(update);
25
+ if (typeof unsub === "function") {
26
+ cleanup = unsub;
27
+ }
28
+
29
+ onUnmounted(() => {
30
+ cleanup?.();
31
+ });
32
+ });
33
+
34
+ return state;
35
+ }
@@ -0,0 +1,36 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+ import { getMutationState, onMutationStateChange } from "../mutation/state";
3
+ import type { MutationState } from "../mutation/state";
4
+
5
+ /**
6
+ * Composable that subscribes to a specific mutation's state changes.
7
+ * Useful for showing per-mutation loading spinners, error states, etc.
8
+ *
9
+ * Usage:
10
+ * const mutation = useSwoffMutationState(mutationId);
11
+ * <div v-if="mutation?.status === 'error'">{{ mutation?.error?.message }}</div>
12
+ */
13
+ export function useSwoffMutationState(id: string | null) {
14
+ const state = ref<MutationState | null>(null);
15
+
16
+ function update() {
17
+ state.value = id ? (getMutationState(id) ?? null) : null;
18
+ }
19
+
20
+ onMounted(update);
21
+
22
+ let cleanup: (() => void) | undefined;
23
+
24
+ onMounted(() => {
25
+ const unsub = onMutationStateChange(update);
26
+ if (typeof unsub === "function") {
27
+ cleanup = unsub;
28
+ }
29
+
30
+ onUnmounted(() => {
31
+ cleanup?.();
32
+ });
33
+ });
34
+
35
+ return state;
36
+ }
@@ -0,0 +1,86 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+ import {
3
+ CONNECTIVITY_EVENT,
4
+ forceRetry,
5
+ getCurrentOnlineStatus,
6
+ } from "../connectivity.js";
7
+
8
+ /**
9
+ * Reactive network information: online status, connection type, and bandwidth.
10
+ *
11
+ * Usage:
12
+ * const { online, wasOffline, lastChangedAt, effectiveType, downlink } = useSwoffNetwork();
13
+ *
14
+ * // Show offline indicator:
15
+ * <div v-if="!online">You are offline</div>
16
+ *
17
+ * // Warn on slow connection:
18
+ * <div v-if="online && effectiveType === '2g'">Slow connection detected</div>
19
+ */
20
+ export function useSwoffNetwork() {
21
+ const online = ref(true);
22
+ const wasOffline = ref(false);
23
+ const lastChangedAt = ref(null);
24
+ const effectiveType = ref(null);
25
+ const downlink = ref(null);
26
+ const isRetrying = ref(false);
27
+
28
+ let wasOfflineFlag = false;
29
+
30
+ onMounted(() => {
31
+ if (!getCurrentOnlineStatus()) {
32
+ wasOfflineFlag = true;
33
+ online.value = false;
34
+ wasOffline.value = true;
35
+ lastChangedAt.value = Date.now();
36
+ }
37
+
38
+ const connection = navigator.connection;
39
+
40
+ function handleConnectivityChange(e) {
41
+ const isTrulyOnline = e.detail.online;
42
+ if (!isTrulyOnline) {
43
+ wasOfflineFlag = true;
44
+ } else {
45
+ wasOfflineFlag = false;
46
+ }
47
+ online.value = isTrulyOnline;
48
+ wasOffline.value = wasOfflineFlag;
49
+ lastChangedAt.value = Date.now();
50
+ }
51
+
52
+ function onTypeChange() {
53
+ if (!connection) return;
54
+ effectiveType.value = connection.effectiveType;
55
+ downlink.value = connection.downlink;
56
+ }
57
+
58
+ window.addEventListener(CONNECTIVITY_EVENT, handleConnectivityChange);
59
+ if (connection) {
60
+ connection.addEventListener("change", onTypeChange);
61
+ }
62
+
63
+ onUnmounted(() => {
64
+ window.removeEventListener(CONNECTIVITY_EVENT, handleConnectivityChange);
65
+ if (connection) {
66
+ connection.removeEventListener("change", onTypeChange);
67
+ }
68
+ });
69
+ });
70
+
71
+ async function retry() {
72
+ isRetrying.value = true;
73
+ await forceRetry();
74
+ isRetrying.value = false;
75
+ }
76
+
77
+ return {
78
+ online,
79
+ wasOffline,
80
+ lastChangedAt,
81
+ effectiveType,
82
+ downlink,
83
+ isRetrying,
84
+ retry,
85
+ };
86
+ }
@@ -0,0 +1,89 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+ import {
3
+ CONNECTIVITY_EVENT,
4
+ forceRetry,
5
+ getCurrentOnlineStatus,
6
+ } from "../connectivity";
7
+
8
+ /**
9
+ * Reactive network information: online status, connection type, and bandwidth.
10
+ *
11
+ * Usage:
12
+ * const { online, wasOffline, lastChangedAt, effectiveType, downlink } = useSwoffNetwork();
13
+ *
14
+ * // Show offline indicator:
15
+ * <div v-if="!online">You are offline</div>
16
+ *
17
+ * // Warn on slow connection:
18
+ * <div v-if="online && effectiveType === '2g'">Slow connection detected</div>
19
+ *
20
+ * @returns {{ online: Ref<boolean>, wasOffline: Ref<boolean>, lastChangedAt: Ref<number | null>, effectiveType: Ref<string | null>, downlink: Ref<number | null>, isRetrying: Ref<boolean>, retry: () => Promise<void> }}
21
+ */
22
+ export function useSwoffNetwork() {
23
+ const online = ref(true);
24
+ const wasOffline = ref(false);
25
+ const lastChangedAt = ref<number | null>(null);
26
+ const effectiveType = ref<string | null>(null);
27
+ const downlink = ref<number | null>(null);
28
+ const isRetrying = ref(false);
29
+
30
+ let wasOfflineFlag = false;
31
+
32
+ onMounted(() => {
33
+ // Sync with actual status after mount — prevents SSR hydration mismatch
34
+ if (!getCurrentOnlineStatus()) {
35
+ wasOfflineFlag = true;
36
+ online.value = false;
37
+ wasOffline.value = true;
38
+ lastChangedAt.value = Date.now();
39
+ }
40
+
41
+ const connection = navigator.connection;
42
+
43
+ function handleConnectivityChange(e: Event) {
44
+ const isTrulyOnline = (e as CustomEvent).detail.online;
45
+ if (!isTrulyOnline) {
46
+ wasOfflineFlag = true;
47
+ } else {
48
+ wasOfflineFlag = false;
49
+ }
50
+ online.value = isTrulyOnline;
51
+ wasOffline.value = wasOfflineFlag;
52
+ lastChangedAt.value = Date.now();
53
+ }
54
+
55
+ function onTypeChange() {
56
+ if (!connection) return;
57
+ effectiveType.value = connection.effectiveType;
58
+ downlink.value = connection.downlink;
59
+ }
60
+
61
+ window.addEventListener(CONNECTIVITY_EVENT, handleConnectivityChange);
62
+ if (connection) {
63
+ connection.addEventListener("change", onTypeChange);
64
+ }
65
+
66
+ onUnmounted(() => {
67
+ window.removeEventListener(CONNECTIVITY_EVENT, handleConnectivityChange);
68
+ if (connection) {
69
+ connection.removeEventListener("change", onTypeChange);
70
+ }
71
+ });
72
+ });
73
+
74
+ async function retry() {
75
+ isRetrying.value = true;
76
+ await forceRetry();
77
+ isRetrying.value = false;
78
+ }
79
+
80
+ return {
81
+ online,
82
+ wasOffline,
83
+ lastChangedAt,
84
+ effectiveType,
85
+ downlink,
86
+ isRetrying,
87
+ retry,
88
+ };
89
+ }
@@ -0,0 +1,19 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+
3
+ export function useSwoffPrecache() {
4
+ const progress = ref(0);
5
+
6
+ function onProgress(e) {
7
+ progress.value = e.detail.percent;
8
+ }
9
+
10
+ onMounted(() => {
11
+ window.addEventListener("sw-progress", onProgress);
12
+ });
13
+
14
+ onUnmounted(() => {
15
+ window.removeEventListener("sw-progress", onProgress);
16
+ });
17
+
18
+ return { progress };
19
+ }
@@ -0,0 +1,19 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+
3
+ export function useSwoffPrecache() {
4
+ const progress = ref(0);
5
+
6
+ function onProgress(e: WindowEventMap["sw-progress"]) {
7
+ progress.value = e.detail.percent;
8
+ }
9
+
10
+ onMounted(() => {
11
+ window.addEventListener("sw-progress", onProgress);
12
+ });
13
+
14
+ onUnmounted(() => {
15
+ window.removeEventListener("sw-progress", onProgress);
16
+ });
17
+
18
+ return { progress };
19
+ }
@@ -0,0 +1,33 @@
1
+ import { ref } from "vue";
2
+ import { prefetchCache } from "../fetch/core.js";
3
+ /**
4
+ * Composable that returns a stable prefetch callback, observable prefetch list, and clear function.
5
+ *
6
+ * Usage:
7
+ * const { prefetch, prefetchList, clear } = useSwoffPrefetch();
8
+ *
9
+ * // Prefetch when user hovers a link:
10
+ * <button @mouseenter="prefetch('/api/todos')">Load</button>
11
+ *
12
+ * // Show how many URLs are prefetched:
13
+ * <span>{{ prefetchList.length }} routes cached</span>
14
+ *
15
+ * // Clear prefetched data:
16
+ * <button @click="clear">Clear prefetch</button>
17
+ */
18
+ export function useSwoffPrefetch() {
19
+ const prefetchList = ref([]);
20
+
21
+ function prefetch(url, options) {
22
+ prefetchCache(url, options);
23
+ if (!prefetchList.value.includes(url)) {
24
+ prefetchList.value = [...prefetchList.value, url];
25
+ }
26
+ }
27
+
28
+ function clear() {
29
+ prefetchList.value = [];
30
+ }
31
+
32
+ return { prefetch, prefetchList: prefetchList.value, clear };
33
+ }
@@ -0,0 +1,41 @@
1
+ import { ref } from "vue";
2
+ import { prefetchCache } from "../fetch/core";
3
+ import type { FetchWithCacheOptions } from "../fetch/core";
4
+
5
+ /**
6
+ * Composable that returns a stable prefetch callback, observable prefetch list, and clear function.
7
+ *
8
+ * Usage:
9
+ * const { prefetch, prefetchList, clear } = useSwoffPrefetch();
10
+ *
11
+ * // Prefetch when user hovers a link:
12
+ * <button @mouseenter="prefetch('/api/todos')">Load</button>
13
+ *
14
+ * // Show how many URLs are prefetched:
15
+ * <span>{{ prefetchList.length }} routes cached</span>
16
+ *
17
+ * // Clear prefetched data:
18
+ * <button @click="clear">Clear prefetch</button>
19
+ *
20
+ * @returns {{ prefetch: (url: string, options?: FetchWithCacheOptions) => void, prefetchList: string[], clear: () => void }}
21
+ */
22
+ export function useSwoffPrefetch(): {
23
+ prefetch: (url: string, options?: FetchWithCacheOptions) => void;
24
+ prefetchList: string[];
25
+ clear: () => void;
26
+ } {
27
+ const prefetchList = ref<string[]>([]);
28
+
29
+ function prefetch(url: string, options?: FetchWithCacheOptions) {
30
+ prefetchCache(url, options);
31
+ if (!prefetchList.value.includes(url)) {
32
+ prefetchList.value = [...prefetchList.value, url];
33
+ }
34
+ }
35
+
36
+ function clear() {
37
+ prefetchList.value = [];
38
+ }
39
+
40
+ return { prefetch, prefetchList: prefetchList.value, clear };
41
+ }