@swoff/cli 0.4.3 → 0.4.4

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 (111) hide show
  1. package/dist/lib/commands/generate-guide.js +15 -15
  2. package/dist/lib/commands/generate-guide.js.map +1 -1
  3. package/dist/lib/commands/init.js +7 -1
  4. package/dist/lib/commands/init.js.map +1 -1
  5. package/dist/lib/generators/file-generators/client-injector-bundle.js +2 -1
  6. package/dist/lib/generators/file-generators/client-injector-bundle.js.map +1 -1
  7. package/dist/lib/generators/file-generators/client-injector.js +2 -1
  8. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  9. package/dist/lib/generators/file-generators/generate-framework-adapters.js +28 -22
  10. package/dist/lib/generators/file-generators/generate-framework-adapters.js.map +1 -1
  11. package/dist/lib/generators/sw-sections/background-precache.js +8 -1
  12. package/dist/lib/generators/sw-sections/background-precache.js.map +1 -1
  13. package/dist/lib/generators/sw-sections/batch-refresh-queue.js +2 -5
  14. package/dist/lib/generators/sw-sections/batch-refresh-queue.js.map +1 -1
  15. package/dist/lib/generators/sw-sections/fetch-handler.js +14 -2
  16. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  17. package/dist/lib/shared/config-types.js +1 -0
  18. package/dist/lib/shared/config-types.js.map +1 -1
  19. package/dist/lib/utils/detect-framework.js +2 -0
  20. package/dist/lib/utils/detect-framework.js.map +1 -1
  21. package/dist/runtime/client-injector-bundle.js +2 -2
  22. package/dist/runtime/client-injector-bundle.js.map +1 -1
  23. package/dist/runtime/client-injector.js +3 -2
  24. package/dist/runtime/client-injector.js.map +1 -1
  25. package/dist/runtime/gql-wrapper.js +3 -1
  26. package/dist/runtime/gql-wrapper.js.map +1 -1
  27. package/dist/runtime/swoff-api-bundle.js +2 -1
  28. package/dist/runtime/swoff-api-bundle.js.map +1 -1
  29. package/package.json +1 -1
  30. package/templates/react/{useOfflineAnalytics.jsx → useSwoffAnalytics.jsx} +3 -3
  31. package/templates/react/{useOfflineAnalytics.tsx → useSwoffAnalytics.tsx} +3 -3
  32. package/templates/react/{useAuth.jsx → useSwoffAuth.jsx} +2 -2
  33. package/templates/react/{useAuth.tsx → useSwoffAuth.tsx} +2 -2
  34. package/templates/react/{useCachedFetch.jsx → useSwoffFetch.jsx} +6 -6
  35. package/templates/react/{useCachedFetch.tsx → useSwoffFetch.tsx} +7 -7
  36. package/templates/react/{useMutation.jsx → useSwoffMutation.jsx} +3 -3
  37. package/templates/react/{useMutation.tsx → useSwoffMutation.tsx} +3 -3
  38. package/templates/react/{useMutationState.jsx → useSwoffMutationState.jsx} +1 -1
  39. package/templates/react/{useMutationState.tsx → useSwoffMutationState.tsx} +2 -2
  40. package/templates/react/{useNetworkStatus.jsx → useSwoffNetwork.jsx} +2 -2
  41. package/templates/react/{useNetworkStatus.tsx → useSwoffNetwork.tsx} +2 -2
  42. package/templates/react/{usePrecacheProgress.jsx → useSwoffPrecache.jsx} +4 -7
  43. package/templates/react/{usePrecacheProgress.tsx → useSwoffPrecache.tsx} +4 -7
  44. package/templates/react/{usePrefetch.jsx → useSwoffPrefetch.jsx} +2 -2
  45. package/templates/react/{usePrefetch.tsx → useSwoffPrefetch.tsx} +2 -2
  46. package/templates/react/{usePushSubscription.jsx → useSwoffPush.jsx} +1 -1
  47. package/templates/react/{usePushSubscription.tsx → useSwoffPush.tsx} +1 -1
  48. package/templates/react/{usePwaInstall.jsx → useSwoffPwa.jsx} +1 -1
  49. package/templates/react/{usePwaInstall.tsx → useSwoffPwa.tsx} +1 -1
  50. package/templates/react/{useMutationQueue.jsx → useSwoffQueue.jsx} +2 -2
  51. package/templates/react/{useMutationQueue.tsx → useSwoffQueue.tsx} +2 -2
  52. package/templates/react/{useStorageEstimate.jsx → useSwoffStorage.jsx} +2 -2
  53. package/templates/react/{useStorageEstimate.tsx → useSwoffStorage.tsx} +2 -2
  54. package/templates/react/{useBackgroundSync.jsx → useSwoffSync.jsx} +1 -1
  55. package/templates/react/{useBackgroundSync.tsx → useSwoffSync.tsx} +1 -1
  56. package/templates/svelte/useSwoffAnalytics.js +31 -0
  57. package/templates/svelte/useSwoffAnalytics.ts +37 -0
  58. package/templates/svelte/useSwoffAuth.js +95 -0
  59. package/templates/svelte/useSwoffAuth.ts +96 -0
  60. package/templates/svelte/useSwoffFetch.js +97 -0
  61. package/templates/svelte/useSwoffFetch.ts +112 -0
  62. package/templates/svelte/useSwoffMutation.js +110 -0
  63. package/templates/svelte/useSwoffMutation.ts +141 -0
  64. package/templates/svelte/useSwoffMutationState.js +27 -0
  65. package/templates/svelte/useSwoffMutationState.ts +28 -0
  66. package/templates/svelte/useSwoffNetwork.js +75 -0
  67. package/templates/svelte/useSwoffNetwork.ts +75 -0
  68. package/templates/svelte/useSwoffPrecache.js +19 -0
  69. package/templates/svelte/useSwoffPrecache.ts +19 -0
  70. package/templates/svelte/useSwoffPrefetch.js +19 -0
  71. package/templates/svelte/useSwoffPrefetch.ts +20 -0
  72. package/templates/svelte/useSwoffPush.js +82 -0
  73. package/templates/svelte/useSwoffPush.ts +82 -0
  74. package/templates/svelte/useSwoffPwa.js +27 -0
  75. package/templates/svelte/useSwoffPwa.ts +27 -0
  76. package/templates/svelte/useSwoffQueue.js +57 -0
  77. package/templates/svelte/useSwoffQueue.ts +67 -0
  78. package/templates/svelte/useSwoffReset.js +21 -0
  79. package/templates/svelte/useSwoffReset.ts +32 -0
  80. package/templates/svelte/useSwoffStorage.js +47 -0
  81. package/templates/svelte/useSwoffStorage.ts +47 -0
  82. package/templates/svelte/useSwoffSync.js +31 -0
  83. package/templates/svelte/useSwoffSync.ts +31 -0
  84. package/templates/vue/useSwoffAnalytics.js +46 -0
  85. package/templates/vue/useSwoffAnalytics.ts +54 -0
  86. package/templates/vue/useSwoffAuth.js +106 -0
  87. package/templates/vue/useSwoffAuth.ts +118 -0
  88. package/templates/vue/useSwoffFetch.js +127 -0
  89. package/templates/vue/useSwoffFetch.ts +143 -0
  90. package/templates/vue/useSwoffMutation.js +124 -0
  91. package/templates/vue/useSwoffMutation.ts +155 -0
  92. package/templates/vue/useSwoffMutationState.js +35 -0
  93. package/templates/vue/useSwoffMutationState.ts +36 -0
  94. package/templates/vue/useSwoffNetwork.js +86 -0
  95. package/templates/vue/useSwoffNetwork.ts +89 -0
  96. package/templates/vue/useSwoffPrecache.js +19 -0
  97. package/templates/vue/useSwoffPrecache.ts +19 -0
  98. package/templates/vue/useSwoffPrefetch.js +33 -0
  99. package/templates/vue/useSwoffPrefetch.ts +41 -0
  100. package/templates/vue/useSwoffPush.js +81 -0
  101. package/templates/vue/useSwoffPush.ts +81 -0
  102. package/templates/vue/useSwoffPwa.js +27 -0
  103. package/templates/vue/useSwoffPwa.ts +27 -0
  104. package/templates/vue/useSwoffQueue.js +70 -0
  105. package/templates/vue/useSwoffQueue.ts +82 -0
  106. package/templates/vue/useSwoffReset.js +32 -0
  107. package/templates/vue/useSwoffReset.ts +39 -0
  108. package/templates/vue/useSwoffStorage.js +56 -0
  109. package/templates/vue/useSwoffStorage.ts +57 -0
  110. package/templates/vue/useSwoffSync.js +30 -0
  111. package/templates/vue/useSwoffSync.ts +30 -0
@@ -0,0 +1,47 @@
1
+ import { writable } from "svelte/store";
2
+ import { onMount } from "svelte";
3
+ import { getStorageEstimate, formatBytes } from "../storage";
4
+
5
+ export function useSwoffStorage(autoRefresh = true) {
6
+ const usage = writable(0);
7
+ const quota = writable(0);
8
+ const percentUsed = writable(0);
9
+ const formattedUsage = writable("0 B");
10
+ const formattedQuota = writable("0 B");
11
+ const loading = writable(true);
12
+ const error = writable<string | null>(null);
13
+
14
+ async function check() {
15
+ try {
16
+ const result = await getStorageEstimate();
17
+ usage.set(result.usage);
18
+ quota.set(result.quota);
19
+ percentUsed.set(result.percentUsed);
20
+ formattedUsage.set(formatBytes(result.usage));
21
+ formattedQuota.set(formatBytes(result.quota));
22
+ loading.set(false);
23
+ error.set(null);
24
+ } catch (err) {
25
+ loading.set(false);
26
+ error.set(String(err));
27
+ }
28
+ }
29
+
30
+ onMount(() => {
31
+ check();
32
+ });
33
+
34
+ if (autoRefresh) {
35
+ onMount(() => {
36
+ function onVisible() {
37
+ if (document.visibilityState === "visible") check();
38
+ }
39
+ document.addEventListener("visibilitychange", onVisible);
40
+ return () => {
41
+ document.removeEventListener("visibilitychange", onVisible);
42
+ };
43
+ });
44
+ }
45
+
46
+ return { usage, quota, percentUsed, formattedUsage, formattedQuota, loading, error };
47
+ }
@@ -0,0 +1,31 @@
1
+ import { writable } from "svelte/store";
2
+ import { onMount } from "svelte";
3
+ import { retrySync } from "../mutation/sync.js";
4
+
5
+ export function useSwoffSync() {
6
+ const supported = writable(
7
+ typeof window !== "undefined" && typeof navigator !== "undefined"
8
+ ? "serviceWorker" in navigator && "SyncManager" in window
9
+ : false,
10
+ );
11
+ const registered = writable(false);
12
+ const lastSync = writable(null);
13
+
14
+ onMount(() => {
15
+ function onSyncComplete(e) {
16
+ registered.set(true);
17
+ lastSync.set({ succeeded: e.detail.succeeded, failed: e.detail.failed });
18
+ }
19
+
20
+ window.addEventListener("background-sync-complete", onSyncComplete);
21
+ return () => {
22
+ window.removeEventListener("background-sync-complete", onSyncComplete);
23
+ };
24
+ });
25
+
26
+ async function triggerSync() {
27
+ await retrySync();
28
+ }
29
+
30
+ return { supported, registered, lastSync, triggerSync };
31
+ }
@@ -0,0 +1,31 @@
1
+ import { writable } from "svelte/store";
2
+ import { onMount } from "svelte";
3
+ import { retrySync } from "../mutation/sync";
4
+
5
+ export function useSwoffSync() {
6
+ const supported = writable(
7
+ typeof window !== "undefined" && typeof navigator !== "undefined"
8
+ ? "serviceWorker" in navigator && "SyncManager" in window
9
+ : false,
10
+ );
11
+ const registered = writable(false);
12
+ const lastSync = writable<{ succeeded: number; failed: number } | null>(null);
13
+
14
+ onMount(() => {
15
+ function onSyncComplete(e: CustomEvent) {
16
+ registered.set(true);
17
+ lastSync.set({ succeeded: e.detail.succeeded, failed: e.detail.failed });
18
+ }
19
+
20
+ window.addEventListener("background-sync-complete", onSyncComplete);
21
+ return () => {
22
+ window.removeEventListener("background-sync-complete", onSyncComplete);
23
+ };
24
+ });
25
+
26
+ async function triggerSync() {
27
+ await retrySync();
28
+ }
29
+
30
+ return { supported, registered, lastSync, triggerSync };
31
+ }
@@ -0,0 +1,46 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+
3
+ /**
4
+ * Tracks offline navigation fallback events dispatched by the Service Worker.
5
+ *
6
+ * Usage:
7
+ * const { lastEvent, events } = useSwoffAnalytics();
8
+ *
9
+ * // Render an offline indicator when a fallback was served:
10
+ * <div v-if="lastEvent">
11
+ * Offline: served {{ lastEvent.fallbackLevel }} for {{ lastEvent.route }}
12
+ * </div>
13
+ *
14
+ * // Or pass a callback:
15
+ * useSwoffAnalytics((event) => console.log("Fallback:", event));
16
+ */
17
+ export function useSwoffAnalytics(callback) {
18
+ const events = ref([]);
19
+ const lastEvent = ref(null);
20
+
21
+ function handler(e) {
22
+ if (e.data?.type === "OFFLINE_FALLBACK_ACTIVATED" && e.data?.detail) {
23
+ const detail = e.data.detail;
24
+ events.value = [...events.value, detail];
25
+ lastEvent.value = detail;
26
+ callback?.(detail);
27
+ }
28
+ }
29
+
30
+ onMounted(() => {
31
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return;
32
+ navigator.serviceWorker.addEventListener("message", handler);
33
+ });
34
+
35
+ onUnmounted(() => {
36
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return;
37
+ navigator.serviceWorker.removeEventListener("message", handler);
38
+ });
39
+
40
+ function clear() {
41
+ events.value = [];
42
+ lastEvent.value = null;
43
+ }
44
+
45
+ return { lastEvent, events, clear };
46
+ }
@@ -0,0 +1,54 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
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 } = useSwoffAnalytics();
14
+ *
15
+ * // Render an offline indicator when a fallback was served:
16
+ * <div v-if="lastEvent">
17
+ * Offline: served {{ lastEvent.fallbackLevel }} for {{ lastEvent.route }}
18
+ * </div>
19
+ *
20
+ * // Or pass a callback:
21
+ * useSwoffAnalytics((event) => console.log("Fallback:", event));
22
+ *
23
+ * @returns {{ lastEvent: Ref<OfflineFallbackEvent | null>, events: Ref<OfflineFallbackEvent[]>, clear: () => void }}
24
+ */
25
+ export function useSwoffAnalytics(callback?: (event: OfflineFallbackEvent) => void) {
26
+ const events = ref<OfflineFallbackEvent[]>([]);
27
+ const lastEvent = ref<OfflineFallbackEvent | null>(null);
28
+
29
+ function handler(e: MessageEvent) {
30
+ if (e.data?.type === "OFFLINE_FALLBACK_ACTIVATED" && e.data?.detail) {
31
+ const detail = e.data.detail as OfflineFallbackEvent;
32
+ events.value = [...events.value, detail];
33
+ lastEvent.value = detail;
34
+ callback?.(detail);
35
+ }
36
+ }
37
+
38
+ onMounted(() => {
39
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return;
40
+ navigator.serviceWorker.addEventListener("message", handler);
41
+ });
42
+
43
+ onUnmounted(() => {
44
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return;
45
+ navigator.serviceWorker.removeEventListener("message", handler);
46
+ });
47
+
48
+ function clear() {
49
+ events.value = [];
50
+ lastEvent.value = null;
51
+ }
52
+
53
+ return { lastEvent, events, clear };
54
+ }
@@ -0,0 +1,106 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+ import { getAuthState } from "../auth/state.js";
3
+ import { setAuth, clearAuth, ensureValidAuth } from "../auth/store.js";
4
+
5
+ /**
6
+ * Reactive auth state with actions: setAuth, clearAuth, ensureValid.
7
+ *
8
+ * Usage:
9
+ * const { authenticated, auth, online, isLoading, error, setAuth, clearAuth, ensureValid } = useSwoffAuth();
10
+ *
11
+ * // Login:
12
+ * const res = await fetch("/api/login", { method: "POST" });
13
+ * const data = await res.json();
14
+ * await setAuth({ token: data.token, user: data.user, expiresAt: data.expiresAt });
15
+ *
16
+ * // Logout:
17
+ * await clearAuth();
18
+ */
19
+ export function useSwoffAuth() {
20
+ const authenticated = ref(false);
21
+ const auth = ref(null);
22
+ const online = ref(typeof navigator !== "undefined" ? navigator.onLine : true);
23
+ const isLoading = ref(false);
24
+ const error = ref(null);
25
+
26
+ async function refreshState() {
27
+ try {
28
+ const authState = await getAuthState();
29
+ authenticated.value = authState.authenticated;
30
+ auth.value = authState.auth;
31
+ error.value = null;
32
+ } catch {
33
+ // ignore — auth not initialized
34
+ }
35
+ }
36
+
37
+ onMounted(() => {
38
+ refreshState();
39
+
40
+ function onOnline() { online.value = true; }
41
+ function onOffline() { online.value = false; }
42
+ function onAuthChange() { refreshState(); }
43
+
44
+ window.addEventListener("online", onOnline);
45
+ window.addEventListener("offline", onOffline);
46
+ window.addEventListener("sw-auth-state-change", onAuthChange);
47
+
48
+ onUnmounted(() => {
49
+ window.removeEventListener("online", onOnline);
50
+ window.removeEventListener("offline", onOffline);
51
+ window.removeEventListener("sw-auth-state-change", onAuthChange);
52
+ });
53
+ });
54
+
55
+ async function doSetAuth(authData) {
56
+ isLoading.value = true;
57
+ error.value = null;
58
+ try {
59
+ await setAuth(authData);
60
+ await refreshState();
61
+ } catch (err) {
62
+ error.value = err instanceof Error ? err : new Error(String(err));
63
+ } finally {
64
+ isLoading.value = false;
65
+ }
66
+ }
67
+
68
+ async function doClearAuth() {
69
+ isLoading.value = true;
70
+ error.value = null;
71
+ try {
72
+ await clearAuth();
73
+ await refreshState();
74
+ } catch (err) {
75
+ error.value = err instanceof Error ? err : new Error(String(err));
76
+ } finally {
77
+ isLoading.value = false;
78
+ }
79
+ }
80
+
81
+ async function doEnsureValid() {
82
+ isLoading.value = true;
83
+ error.value = null;
84
+ try {
85
+ const result = await ensureValidAuth();
86
+ await refreshState();
87
+ return result;
88
+ } catch (err) {
89
+ error.value = err instanceof Error ? err : new Error(String(err));
90
+ return null;
91
+ } finally {
92
+ isLoading.value = false;
93
+ }
94
+ }
95
+
96
+ return {
97
+ authenticated,
98
+ auth,
99
+ online,
100
+ isLoading,
101
+ error,
102
+ setAuth: doSetAuth,
103
+ clearAuth: doClearAuth,
104
+ ensureValid: doEnsureValid,
105
+ };
106
+ }
@@ -0,0 +1,118 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+ import { getAuthState } from "../auth/state";
3
+ import { setAuth, clearAuth, ensureValidAuth } from "../auth/store";
4
+ import type { AuthData } from "../auth/adapter";
5
+
6
+ /**
7
+ * Reactive auth state with actions: setAuth, clearAuth, ensureValid.
8
+ *
9
+ * Listens for sw-auth-state-change events (dispatched by clearAuth(),
10
+ * setAuth(), or the SW's AUTH_CLEARED broadcast).
11
+ *
12
+ * Usage:
13
+ * const { authenticated, auth, online, isLoading, error, setAuth, clearAuth, ensureValid } = useSwoffAuth();
14
+ *
15
+ * // Login:
16
+ * const res = await fetch("/api/login", { method: "POST" });
17
+ * const data = await res.json();
18
+ * await setAuth({ token: data.token, user: data.user, expiresAt: data.expiresAt });
19
+ *
20
+ * // Access user data (typed via AuthData):
21
+ * const userName = auth?.user?.name;
22
+ *
23
+ * // Logout — clears tokens from memory and user cache from IndexedDB:
24
+ * await clearAuth();
25
+ *
26
+ * // Silently refresh the session (page restore):
27
+ * const authData = await ensureValid();
28
+ *
29
+ * @returns {{ authenticated, auth, online, isLoading, error, setAuth, clearAuth, ensureValid }}
30
+ */
31
+ export function useSwoffAuth() {
32
+ const authenticated = ref(false);
33
+ const auth = ref<AuthData | null>(null);
34
+ const online = ref(typeof navigator !== "undefined" ? navigator.onLine : true);
35
+ const isLoading = ref(false);
36
+ const error = ref<Error | null>(null);
37
+
38
+ async function refreshState() {
39
+ try {
40
+ const authState = await getAuthState();
41
+ authenticated.value = authState.authenticated;
42
+ auth.value = authState.auth;
43
+ error.value = null;
44
+ } catch {
45
+ // ignore — auth not initialized
46
+ }
47
+ }
48
+
49
+ onMounted(() => {
50
+ refreshState();
51
+
52
+ function onOnline() { online.value = true; }
53
+ function onOffline() { online.value = false; }
54
+ function onAuthChange() { refreshState(); }
55
+
56
+ window.addEventListener("online", onOnline);
57
+ window.addEventListener("offline", onOffline);
58
+ window.addEventListener("sw-auth-state-change", onAuthChange);
59
+
60
+ onUnmounted(() => {
61
+ window.removeEventListener("online", onOnline);
62
+ window.removeEventListener("offline", onOffline);
63
+ window.removeEventListener("sw-auth-state-change", onAuthChange);
64
+ });
65
+ });
66
+
67
+ async function doSetAuth(authData: AuthData) {
68
+ isLoading.value = true;
69
+ error.value = null;
70
+ try {
71
+ await setAuth(authData);
72
+ await refreshState();
73
+ } catch (err) {
74
+ error.value = err instanceof Error ? err : new Error(String(err));
75
+ } finally {
76
+ isLoading.value = false;
77
+ }
78
+ }
79
+
80
+ async function doClearAuth() {
81
+ isLoading.value = true;
82
+ error.value = null;
83
+ try {
84
+ await clearAuth();
85
+ await refreshState();
86
+ } catch (err) {
87
+ error.value = err instanceof Error ? err : new Error(String(err));
88
+ } finally {
89
+ isLoading.value = false;
90
+ }
91
+ }
92
+
93
+ async function doEnsureValid(): Promise<AuthData | null> {
94
+ isLoading.value = true;
95
+ error.value = null;
96
+ try {
97
+ const result = await ensureValidAuth();
98
+ await refreshState();
99
+ return result;
100
+ } catch (err) {
101
+ error.value = err instanceof Error ? err : new Error(String(err));
102
+ return null;
103
+ } finally {
104
+ isLoading.value = false;
105
+ }
106
+ }
107
+
108
+ return {
109
+ authenticated,
110
+ auth,
111
+ online,
112
+ isLoading,
113
+ error,
114
+ setAuth: doSetAuth,
115
+ clearAuth: doClearAuth,
116
+ ensureValid: doEnsureValid,
117
+ };
118
+ }
@@ -0,0 +1,127 @@
1
+ import { ref, onMounted, onUnmounted, watch } from "vue";
2
+ import { fetchWithCache } from "../fetch/core.js";
3
+ import { generateTags } from "../cache/tags.js";
4
+
5
+ /**
6
+ * Fetch and cache data with automatic invalidation, revalidation, and offline support.
7
+ *
8
+ * Usage:
9
+ * const { data, error, loading, refetch } = useSwoffFetch("/api/todos");
10
+ *
11
+ * // Select/transform data
12
+ * const { data: names } = useSwoffFetch("/api/users", {
13
+ * select: (users) => users.map(u => u.name),
14
+ * });
15
+ *
16
+ * // Keep previous data while fetching the next page
17
+ * const { data } = useSwoffFetch(`/api/items?page=${page}`, {
18
+ * keepPreviousData: true,
19
+ * });
20
+ */
21
+ export function useSwoffFetch(url, options = {}) {
22
+ const {
23
+ select,
24
+ retry: retryOpt,
25
+ placeholderData,
26
+ onSuccess,
27
+ onError,
28
+ ...fetchOptions
29
+ } = options;
30
+
31
+ const data = ref(placeholderData ?? null);
32
+ const error = ref(null);
33
+ const loading = ref(url != null && options.enabled !== false);
34
+ const refetchCount = ref(0);
35
+
36
+ let cachedData = placeholderData ?? null;
37
+
38
+ const isEnabled = options.enabled !== false && url != null;
39
+
40
+ function refetch() {
41
+ refetchCount.value++;
42
+ }
43
+
44
+ watch(
45
+ () => url,
46
+ () => {
47
+ if (isEnabled) refetch();
48
+ },
49
+ );
50
+
51
+ watch(
52
+ [() => url, () => refetchCount.value],
53
+ () => {
54
+ if (!isEnabled) return;
55
+
56
+ let cancelled = false;
57
+ let retriesLeft =
58
+ retryOpt === true
59
+ ? Infinity
60
+ : typeof retryOpt === "number"
61
+ ? retryOpt
62
+ : 0;
63
+
64
+ const doFetch = async () => {
65
+ loading.value = true;
66
+ try {
67
+ const { response } = await fetchWithCache(url, fetchOptions);
68
+ if (cancelled) return;
69
+ if (response) {
70
+ if (!response.ok) {
71
+ let errorMessage = `HTTP ${response.status}`;
72
+ try {
73
+ const errBody = await response.json();
74
+ errorMessage = errBody?.message || errBody?.error || errorMessage;
75
+ } catch {}
76
+ throw new Error(errorMessage);
77
+ }
78
+ const raw = await response.json();
79
+ const selected = select ? select(raw) : raw;
80
+ data.value = selected;
81
+ cachedData = selected;
82
+ if (!cancelled) onSuccess?.(selected);
83
+ } else {
84
+ data.value = null;
85
+ cachedData = null;
86
+ }
87
+ if (!cancelled) error.value = null;
88
+ } catch (err) {
89
+ if (!cancelled && retriesLeft > 0) {
90
+ retriesLeft--;
91
+ await new Promise((r) => setTimeout(r, 1000));
92
+ return doFetch();
93
+ }
94
+ if (!cancelled) {
95
+ const e = err instanceof Error ? err : new Error(String(err));
96
+ error.value = e;
97
+ onError?.(e);
98
+ }
99
+ } finally {
100
+ if (!cancelled) loading.value = false;
101
+ }
102
+ };
103
+
104
+ doFetch();
105
+ },
106
+ );
107
+
108
+ if (isEnabled && url) {
109
+ onMounted(() => {
110
+ function onInvalidated(e) {
111
+ const detail = e.detail;
112
+ const tags = detail?.tags;
113
+ if (!tags || !Array.isArray(tags)) return;
114
+ const urlTags = generateTags(url);
115
+ if (tags.some((t) => urlTags.includes(t))) {
116
+ refetch();
117
+ }
118
+ }
119
+ window.addEventListener("cache-invalidated", onInvalidated);
120
+ onUnmounted(() => {
121
+ window.removeEventListener("cache-invalidated", onInvalidated);
122
+ });
123
+ });
124
+ }
125
+
126
+ return { data, error, loading, refetch };
127
+ }
@@ -0,0 +1,143 @@
1
+ import { ref, onMounted, onUnmounted, watch } from "vue";
2
+ import { fetchWithCache } from "../fetch/core";
3
+ import { generateTags } from "../cache/tags";
4
+ import type { FetchWithCacheOptions } from "../fetch/core";
5
+
6
+ /**
7
+ * Fetch and cache data with automatic invalidation, revalidation, and offline support.
8
+ *
9
+ * Usage:
10
+ * const { data, error, loading, refetch } = useSwoffFetch("/api/todos");
11
+ *
12
+ * // Select/transform data
13
+ * const { data: names } = useSwoffFetch("/api/users", {
14
+ * select: (users) => users.map(u => u.name),
15
+ * });
16
+ *
17
+ * // Keep previous data while fetching the next page
18
+ * const { data } = useSwoffFetch(`/api/items?page=${page}`, {
19
+ * keepPreviousData: true,
20
+ * });
21
+ *
22
+ * @param url - URL to fetch. Pass null to skip fetching.
23
+ * @param options - Fetch options plus: select, keepPreviousData, retry, placeholderData, onSuccess, onError
24
+ */
25
+ export function useSwoffFetch<T = unknown, R = T>(
26
+ url: string | null,
27
+ options: FetchWithCacheOptions & {
28
+ enabled?: boolean;
29
+ select?: (data: T) => R;
30
+ keepPreviousData?: boolean;
31
+ retry?: number | boolean;
32
+ placeholderData?: R;
33
+ onSuccess?: (data: R) => void;
34
+ onError?: (error: Error) => void;
35
+ } = {},
36
+ ) {
37
+ const {
38
+ select,
39
+ retry: retryOpt,
40
+ placeholderData,
41
+ onSuccess,
42
+ onError,
43
+ ...fetchOptions
44
+ } = options;
45
+
46
+ const data = ref<R | null>(placeholderData ?? null);
47
+ const error = ref<Error | null>(null);
48
+ const loading = ref(url != null && options.enabled !== false);
49
+ const refetchCount = ref(0);
50
+
51
+ let cachedData: R | null = placeholderData ?? null;
52
+
53
+ const isEnabled = options.enabled !== false && url != null;
54
+
55
+ function refetch() {
56
+ refetchCount.value++;
57
+ }
58
+
59
+ watch(
60
+ () => url,
61
+ () => {
62
+ if (isEnabled) refetch();
63
+ },
64
+ );
65
+
66
+ watch(
67
+ [() => url, () => refetchCount.value],
68
+ () => {
69
+ if (!isEnabled) return;
70
+
71
+ let cancelled = false;
72
+ let retriesLeft: number =
73
+ retryOpt === true
74
+ ? Infinity
75
+ : typeof retryOpt === "number"
76
+ ? retryOpt
77
+ : 0;
78
+
79
+ const doFetch = async () => {
80
+ loading.value = true;
81
+ try {
82
+ const { response } = await fetchWithCache(url, fetchOptions);
83
+ if (cancelled) return;
84
+ if (response) {
85
+ if (!response.ok) {
86
+ let errorMessage = `HTTP ${response.status}`;
87
+ try {
88
+ const errBody = await response.json();
89
+ errorMessage = errBody?.message || errBody?.error || errorMessage;
90
+ } catch {}
91
+ throw new Error(errorMessage);
92
+ }
93
+ const raw: T = await response.json();
94
+ const selected = select ? select(raw) : (raw as unknown as R);
95
+ data.value = selected;
96
+ cachedData = selected;
97
+ if (!cancelled) onSuccess?.(selected);
98
+ } else {
99
+ data.value = null;
100
+ cachedData = null;
101
+ }
102
+ if (!cancelled) error.value = null;
103
+ } catch (err) {
104
+ if (!cancelled && retriesLeft > 0) {
105
+ retriesLeft--;
106
+ await new Promise((r) => setTimeout(r, 1000));
107
+ return doFetch();
108
+ }
109
+ if (!cancelled) {
110
+ const e = err instanceof Error ? err : new Error(String(err));
111
+ error.value = e;
112
+ onError?.(e);
113
+ }
114
+ } finally {
115
+ if (!cancelled) loading.value = false;
116
+ }
117
+ };
118
+
119
+ doFetch();
120
+ },
121
+ );
122
+
123
+ // Auto-refetch on cache invalidation
124
+ if (isEnabled && url) {
125
+ onMounted(() => {
126
+ function onInvalidated(e: Event) {
127
+ const detail = (e as CustomEvent).detail;
128
+ const tags = detail?.tags;
129
+ if (!tags || !Array.isArray(tags)) return;
130
+ const urlTags = generateTags(url!);
131
+ if (tags.some((t: string) => urlTags.includes(t))) {
132
+ refetch();
133
+ }
134
+ }
135
+ window.addEventListener("cache-invalidated", onInvalidated);
136
+ onUnmounted(() => {
137
+ window.removeEventListener("cache-invalidated", onInvalidated);
138
+ });
139
+ });
140
+ }
141
+
142
+ return { data, error, loading, refetch };
143
+ }