@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,95 @@
1
+ import { writable } from "svelte/store";
2
+ import { onMount } from "svelte";
3
+ import { getAuthState } from "../auth/state.js";
4
+ import { setAuth, clearAuth, ensureValidAuth } from "../auth/store.js";
5
+
6
+ export function useSwoffAuth() {
7
+ const authenticated = writable(false);
8
+ const auth = writable(null);
9
+ const online = writable(
10
+ typeof navigator !== "undefined" ? navigator.onLine : true,
11
+ );
12
+ const isLoading = writable(false);
13
+ const error = writable(null);
14
+
15
+ async function refreshState() {
16
+ try {
17
+ const authState = await getAuthState();
18
+ authenticated.set(authState.authenticated);
19
+ auth.set(authState.auth);
20
+ error.set(null);
21
+ } catch {
22
+ // ignore — auth not initialized
23
+ }
24
+ }
25
+
26
+ onMount(() => {
27
+ refreshState();
28
+
29
+ function onOnline() { online.set(true); }
30
+ function onOffline() { online.set(false); }
31
+ function onAuthChange() { refreshState(); }
32
+
33
+ window.addEventListener("online", onOnline);
34
+ window.addEventListener("offline", onOffline);
35
+ window.addEventListener("sw-auth-state-change", onAuthChange);
36
+
37
+ return () => {
38
+ window.removeEventListener("online", onOnline);
39
+ window.removeEventListener("offline", onOffline);
40
+ window.removeEventListener("sw-auth-state-change", onAuthChange);
41
+ };
42
+ });
43
+
44
+ async function doSetAuth(authData) {
45
+ isLoading.set(true);
46
+ error.set(null);
47
+ try {
48
+ await setAuth(authData);
49
+ await refreshState();
50
+ } catch (err) {
51
+ error.set(err instanceof Error ? err : new Error(String(err)));
52
+ } finally {
53
+ isLoading.set(false);
54
+ }
55
+ }
56
+
57
+ async function doClearAuth() {
58
+ isLoading.set(true);
59
+ error.set(null);
60
+ try {
61
+ await clearAuth();
62
+ await refreshState();
63
+ } catch (err) {
64
+ error.set(err instanceof Error ? err : new Error(String(err)));
65
+ } finally {
66
+ isLoading.set(false);
67
+ }
68
+ }
69
+
70
+ async function doEnsureValid() {
71
+ isLoading.set(true);
72
+ error.set(null);
73
+ try {
74
+ const result = await ensureValidAuth();
75
+ await refreshState();
76
+ return result;
77
+ } catch (err) {
78
+ error.set(err instanceof Error ? err : new Error(String(err)));
79
+ return null;
80
+ } finally {
81
+ isLoading.set(false);
82
+ }
83
+ }
84
+
85
+ return {
86
+ authenticated,
87
+ auth,
88
+ online,
89
+ isLoading,
90
+ error,
91
+ setAuth: doSetAuth,
92
+ clearAuth: doClearAuth,
93
+ ensureValid: doEnsureValid,
94
+ };
95
+ }
@@ -0,0 +1,96 @@
1
+ import { writable } from "svelte/store";
2
+ import { onMount } from "svelte";
3
+ import { getAuthState } from "../auth/state";
4
+ import { setAuth, clearAuth, ensureValidAuth } from "../auth/store";
5
+ import type { AuthData } from "../auth/adapter";
6
+
7
+ export function useSwoffAuth() {
8
+ const authenticated = writable(false);
9
+ const auth = writable<AuthData | null>(null);
10
+ const online = writable(
11
+ typeof navigator !== "undefined" ? navigator.onLine : true,
12
+ );
13
+ const isLoading = writable(false);
14
+ const error = writable<Error | null>(null);
15
+
16
+ async function refreshState() {
17
+ try {
18
+ const authState = await getAuthState();
19
+ authenticated.set(authState.authenticated);
20
+ auth.set(authState.auth);
21
+ error.set(null);
22
+ } catch {
23
+ // ignore — auth not initialized
24
+ }
25
+ }
26
+
27
+ onMount(() => {
28
+ refreshState();
29
+
30
+ function onOnline() { online.set(true); }
31
+ function onOffline() { online.set(false); }
32
+ function onAuthChange() { refreshState(); }
33
+
34
+ window.addEventListener("online", onOnline);
35
+ window.addEventListener("offline", onOffline);
36
+ window.addEventListener("sw-auth-state-change", onAuthChange);
37
+
38
+ return () => {
39
+ window.removeEventListener("online", onOnline);
40
+ window.removeEventListener("offline", onOffline);
41
+ window.removeEventListener("sw-auth-state-change", onAuthChange);
42
+ };
43
+ });
44
+
45
+ async function doSetAuth(authData: AuthData) {
46
+ isLoading.set(true);
47
+ error.set(null);
48
+ try {
49
+ await setAuth(authData);
50
+ await refreshState();
51
+ } catch (err) {
52
+ error.set(err instanceof Error ? err : new Error(String(err)));
53
+ } finally {
54
+ isLoading.set(false);
55
+ }
56
+ }
57
+
58
+ async function doClearAuth() {
59
+ isLoading.set(true);
60
+ error.set(null);
61
+ try {
62
+ await clearAuth();
63
+ await refreshState();
64
+ } catch (err) {
65
+ error.set(err instanceof Error ? err : new Error(String(err)));
66
+ } finally {
67
+ isLoading.set(false);
68
+ }
69
+ }
70
+
71
+ async function doEnsureValid(): Promise<AuthData | null> {
72
+ isLoading.set(true);
73
+ error.set(null);
74
+ try {
75
+ const result = await ensureValidAuth();
76
+ await refreshState();
77
+ return result;
78
+ } catch (err) {
79
+ error.set(err instanceof Error ? err : new Error(String(err)));
80
+ return null;
81
+ } finally {
82
+ isLoading.set(false);
83
+ }
84
+ }
85
+
86
+ return {
87
+ authenticated,
88
+ auth,
89
+ online,
90
+ isLoading,
91
+ error,
92
+ setAuth: doSetAuth,
93
+ clearAuth: doClearAuth,
94
+ ensureValid: doEnsureValid,
95
+ };
96
+ }
@@ -0,0 +1,97 @@
1
+ import { writable } from "svelte/store";
2
+ import { onMount } from "svelte";
3
+ import { fetchWithCache } from "../fetch/core.js";
4
+ import { generateTags } from "../cache/tags.js";
5
+
6
+ export function useSwoffFetch(url, options = {}) {
7
+ const {
8
+ select,
9
+ retry: retryOpt,
10
+ placeholderData,
11
+ onSuccess,
12
+ onError,
13
+ ...fetchOptions
14
+ } = options;
15
+
16
+ const data = writable(placeholderData ?? null);
17
+ const error = writable(null);
18
+ const loading = writable(url != null && options.enabled !== false);
19
+
20
+ const isEnabled = options.enabled !== false && url != null;
21
+ let refetchCount = 0;
22
+
23
+ function refetch() {
24
+ refetchCount++;
25
+ doFetch();
26
+ }
27
+
28
+ async function doFetch() {
29
+ if (!isEnabled) return;
30
+ const currentFetchId = refetchCount;
31
+
32
+ let retriesLeft =
33
+ retryOpt === true
34
+ ? Infinity
35
+ : typeof retryOpt === "number"
36
+ ? retryOpt
37
+ : 0;
38
+
39
+ loading.set(true);
40
+ try {
41
+ const { response } = await fetchWithCache(url, fetchOptions);
42
+ if (currentFetchId !== refetchCount) return;
43
+ if (response) {
44
+ if (!response.ok) {
45
+ let errorMessage = `HTTP ${response.status}`;
46
+ try {
47
+ const errBody = await response.json();
48
+ errorMessage = errBody?.message || errBody?.error || errorMessage;
49
+ } catch {}
50
+ throw new Error(errorMessage);
51
+ }
52
+ const raw = await response.json();
53
+ const selected = select ? select(raw) : raw;
54
+ data.set(selected);
55
+ error.set(null);
56
+ onSuccess?.(selected);
57
+ } else {
58
+ data.set(null);
59
+ }
60
+ } catch (err) {
61
+ if (currentFetchId !== refetchCount) return;
62
+ if (retriesLeft > 0) {
63
+ retriesLeft--;
64
+ await new Promise((r) => setTimeout(r, 1000));
65
+ return doFetch();
66
+ }
67
+ const e = err instanceof Error ? err : new Error(String(err));
68
+ error.set(e);
69
+ onError?.(e);
70
+ } finally {
71
+ if (currentFetchId === refetchCount) {
72
+ loading.set(false);
73
+ }
74
+ }
75
+ }
76
+
77
+ onMount(() => {
78
+ if (isEnabled) doFetch();
79
+
80
+ function onInvalidated(e) {
81
+ const detail = e.detail;
82
+ const tags = detail?.tags;
83
+ if (!tags || !Array.isArray(tags)) return;
84
+ const urlTags = generateTags(url);
85
+ if (tags.some((t) => urlTags.includes(t))) {
86
+ refetch();
87
+ }
88
+ }
89
+
90
+ window.addEventListener("cache-invalidated", onInvalidated);
91
+ return () => {
92
+ window.removeEventListener("cache-invalidated", onInvalidated);
93
+ };
94
+ });
95
+
96
+ return { data, error, loading, refetch };
97
+ }
@@ -0,0 +1,112 @@
1
+ import { writable } from "svelte/store";
2
+ import { onMount } from "svelte";
3
+ import { fetchWithCache } from "../fetch/core";
4
+ import { generateTags } from "../cache/tags";
5
+ import type { FetchWithCacheOptions } from "../fetch/core";
6
+
7
+ export function useSwoffFetch<T = unknown, R = T>(
8
+ url: string | null,
9
+ options: FetchWithCacheOptions & {
10
+ enabled?: boolean;
11
+ select?: (data: T) => R;
12
+ keepPreviousData?: boolean;
13
+ retry?: number | boolean;
14
+ placeholderData?: R;
15
+ onSuccess?: (data: R) => void;
16
+ onError?: (error: Error) => void;
17
+ } = {},
18
+ ) {
19
+ const {
20
+ select,
21
+ retry: retryOpt,
22
+ placeholderData,
23
+ onSuccess,
24
+ onError,
25
+ ...fetchOptions
26
+ } = options;
27
+
28
+ const data = writable<R | null>(placeholderData ?? null);
29
+ const error = writable<Error | null>(null);
30
+ const loading = writable(url != null && options.enabled !== false);
31
+
32
+ const isEnabled = options.enabled !== false && url != null;
33
+ let refetchCount = 0;
34
+
35
+ function refetch() {
36
+ refetchCount++;
37
+ doFetch();
38
+ }
39
+
40
+ let cancelled = false;
41
+
42
+ async function doFetch() {
43
+ if (!isEnabled) return;
44
+ const currentFetchId = refetchCount;
45
+
46
+ let retriesLeft: number =
47
+ retryOpt === true
48
+ ? Infinity
49
+ : typeof retryOpt === "number"
50
+ ? retryOpt
51
+ : 0;
52
+
53
+ loading.set(true);
54
+ try {
55
+ const { response } = await fetchWithCache(url, fetchOptions);
56
+ if (currentFetchId !== refetchCount) return;
57
+ if (response) {
58
+ if (!response.ok) {
59
+ let errorMessage = `HTTP ${response.status}`;
60
+ try {
61
+ const errBody = await response.json();
62
+ errorMessage = errBody?.message || errBody?.error || errorMessage;
63
+ } catch {}
64
+ throw new Error(errorMessage);
65
+ }
66
+ const raw: T = await response.json();
67
+ const selected = select ? select(raw) : (raw as unknown as R);
68
+ data.set(selected);
69
+ error.set(null);
70
+ onSuccess?.(selected);
71
+ } else {
72
+ data.set(null);
73
+ }
74
+ } catch (err) {
75
+ if (currentFetchId !== refetchCount) return;
76
+ if (retriesLeft > 0) {
77
+ retriesLeft--;
78
+ await new Promise((r) => setTimeout(r, 1000));
79
+ return doFetch();
80
+ }
81
+ const e = err instanceof Error ? err : new Error(String(err));
82
+ error.set(e);
83
+ onError?.(e);
84
+ } finally {
85
+ if (currentFetchId === refetchCount) {
86
+ loading.set(false);
87
+ }
88
+ }
89
+ }
90
+
91
+ onMount(() => {
92
+ if (isEnabled) doFetch();
93
+
94
+ function onInvalidated(e: Event) {
95
+ const detail = (e as CustomEvent).detail;
96
+ const tags = detail?.tags;
97
+ if (!tags || !Array.isArray(tags)) return;
98
+ const urlTags = generateTags(url!);
99
+ if (tags.some((t: string) => urlTags.includes(t))) {
100
+ refetch();
101
+ }
102
+ }
103
+
104
+ window.addEventListener("cache-invalidated", onInvalidated);
105
+ return () => {
106
+ cancelled = true;
107
+ window.removeEventListener("cache-invalidated", onInvalidated);
108
+ };
109
+ });
110
+
111
+ return { data, error, loading, refetch };
112
+ }
@@ -0,0 +1,110 @@
1
+ import { writable } from "svelte/store";
2
+ import { fetchWithCache } from "../fetch/core.js";
3
+ import {
4
+ trackMutation,
5
+ resolveMutation,
6
+ rejectMutation,
7
+ } from "../mutation/state.js";
8
+
9
+ export function useSwoffMutation(url, options = {}) {
10
+ const lastResult = writable(null);
11
+ const isLoading = writable(false);
12
+ const mutationId = writable(null);
13
+
14
+ const inFlightKeys = new Set();
15
+
16
+ async function mutate(body, callbacks) {
17
+ const key = options.mutationKey;
18
+ if (key && inFlightKeys.has(key)) return { status: "skipped" };
19
+ if (key) inFlightKeys.add(key);
20
+
21
+ let retriesLeft =
22
+ options.retry === true
23
+ ? Infinity
24
+ : typeof options.retry === "number"
25
+ ? options.retry
26
+ : 0;
27
+
28
+ const mid = "mut-" + crypto.randomUUID();
29
+ mutationId.set(mid);
30
+ trackMutation(mid, "pending");
31
+ isLoading.set(true);
32
+
33
+ options.onMutate?.();
34
+
35
+ const attempt = async () => {
36
+ try {
37
+ const {
38
+ mutationKey,
39
+ onMutate,
40
+ onSuccess,
41
+ onError,
42
+ onSettled,
43
+ retry,
44
+ ...fetchOnlyOptions
45
+ } = options;
46
+ const fetchOptions = { ...fetchOnlyOptions, body };
47
+ const { response, queued } = await fetchWithCache(url, fetchOptions);
48
+ if (queued) {
49
+ resolveMutation(mid, null);
50
+ isLoading.set(false);
51
+ const r = { status: "queued" };
52
+ lastResult.set(r);
53
+ options.onSettled?.();
54
+ callbacks?.onSettled?.();
55
+ if (key) inFlightKeys.delete(key);
56
+ return r;
57
+ }
58
+ const data = await response.json();
59
+ resolveMutation(mid, data);
60
+ isLoading.set(false);
61
+ const r = { status: "success", data };
62
+ lastResult.set(r);
63
+ options.onSuccess?.(data);
64
+ callbacks?.onSuccess?.(data);
65
+ options.onSettled?.();
66
+ callbacks?.onSettled?.();
67
+ if (key) inFlightKeys.delete(key);
68
+ return r;
69
+ } catch (err) {
70
+ if (retriesLeft > 0) {
71
+ retriesLeft--;
72
+ await new Promise((r) => setTimeout(r, 1000));
73
+ return attempt();
74
+ }
75
+ const error = err instanceof Error ? err : new Error(String(err));
76
+ rejectMutation(mid, error);
77
+ isLoading.set(false);
78
+ const r = { status: "error", error };
79
+ lastResult.set(r);
80
+ options.onError?.(error);
81
+ callbacks?.onError?.(error);
82
+ options.onSettled?.();
83
+ callbacks?.onSettled?.();
84
+ if (key) inFlightKeys.delete(key);
85
+ return r;
86
+ }
87
+ };
88
+
89
+ return attempt();
90
+ }
91
+
92
+ function reset() {
93
+ lastResult.set(null);
94
+ isLoading.set(false);
95
+ }
96
+
97
+ return {
98
+ mutate,
99
+ isLoading,
100
+ isIdle: lastResult,
101
+ isQueued: lastResult,
102
+ isSuccess: lastResult,
103
+ isError: lastResult,
104
+ data: lastResult,
105
+ error: lastResult,
106
+ lastResult,
107
+ reset,
108
+ mutationId,
109
+ };
110
+ }
@@ -0,0 +1,141 @@
1
+ import { writable } from "svelte/store";
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
+ export function useSwoffMutation<TData = unknown>(
32
+ url: string,
33
+ options: UseMutationOptions<TData> = {},
34
+ ) {
35
+ const lastResult = writable<MutateResult<TData> | null>(null);
36
+ const isLoading = writable(false);
37
+ const mutationId = writable<string | null>(null);
38
+
39
+ const inFlightKeys = new Set<string>();
40
+
41
+ async function mutate(
42
+ body?: unknown,
43
+ callbacks?: MutateCallbacks<TData>,
44
+ ): Promise<MutateResult<TData>> {
45
+ const key = options.mutationKey;
46
+ if (key && inFlightKeys.has(key)) return { status: "skipped" };
47
+ if (key) inFlightKeys.add(key);
48
+
49
+ let retriesLeft: number =
50
+ options.retry === true
51
+ ? Infinity
52
+ : typeof options.retry === "number"
53
+ ? options.retry
54
+ : 0;
55
+
56
+ const mid = "mut-" + crypto.randomUUID();
57
+ mutationId.set(mid);
58
+ trackMutation(mid, "pending");
59
+ isLoading.set(true);
60
+
61
+ options.onMutate?.();
62
+
63
+ const attempt = async (): Promise<MutateResult<TData>> => {
64
+ try {
65
+ const {
66
+ mutationKey,
67
+ onMutate,
68
+ onSuccess,
69
+ onError,
70
+ onSettled,
71
+ retry,
72
+ ...fetchOnlyOptions
73
+ } = options;
74
+ const fetchOptions = { ...fetchOnlyOptions, body };
75
+ const { response, queued } = await fetchWithCache<TData>(
76
+ url,
77
+ fetchOptions as FetchWithCacheOptions,
78
+ );
79
+ if (queued) {
80
+ resolveMutation(mid, null);
81
+ isLoading.set(false);
82
+ const r: MutateResult<TData> = { status: "queued" };
83
+ lastResult.set(r);
84
+ options.onSettled?.();
85
+ callbacks?.onSettled?.();
86
+ if (key) inFlightKeys.delete(key);
87
+ return r;
88
+ }
89
+ const data: TData = await response.json();
90
+ resolveMutation(mid, data);
91
+ isLoading.set(false);
92
+ const r: MutateResult<TData> = { status: "success", data };
93
+ lastResult.set(r);
94
+ options.onSuccess?.(data);
95
+ callbacks?.onSuccess?.(data);
96
+ options.onSettled?.();
97
+ callbacks?.onSettled?.();
98
+ if (key) inFlightKeys.delete(key);
99
+ return r;
100
+ } catch (err) {
101
+ if (retriesLeft > 0) {
102
+ retriesLeft--;
103
+ await new Promise((r) => setTimeout(r, 1000));
104
+ return attempt();
105
+ }
106
+ const error = err instanceof Error ? err : new Error(String(err));
107
+ rejectMutation(mid, error);
108
+ isLoading.set(false);
109
+ const r: MutateResult<TData> = { status: "error", error };
110
+ lastResult.set(r);
111
+ options.onError?.(error);
112
+ callbacks?.onError?.(error);
113
+ options.onSettled?.();
114
+ callbacks?.onSettled?.();
115
+ if (key) inFlightKeys.delete(key);
116
+ return r;
117
+ }
118
+ };
119
+
120
+ return attempt();
121
+ }
122
+
123
+ function reset() {
124
+ lastResult.set(null);
125
+ isLoading.set(false);
126
+ }
127
+
128
+ return {
129
+ mutate,
130
+ isLoading,
131
+ isIdle: lastResult,
132
+ isQueued: lastResult,
133
+ isSuccess: lastResult,
134
+ isError: lastResult,
135
+ data: lastResult,
136
+ error: lastResult,
137
+ lastResult,
138
+ reset,
139
+ mutationId,
140
+ };
141
+ }
@@ -0,0 +1,27 @@
1
+ import { writable } from "svelte/store";
2
+ import { onMount } from "svelte";
3
+ import { getMutationState, onMutationStateChange } from "../mutation/state.js";
4
+
5
+ export function useSwoffMutationState(id) {
6
+ const state = writable(null);
7
+
8
+ function update() {
9
+ state.set(id ? (getMutationState(id) ?? null) : null);
10
+ }
11
+
12
+ onMount(update);
13
+
14
+ let cleanup;
15
+
16
+ onMount(() => {
17
+ const unsub = onMutationStateChange(update);
18
+ if (typeof unsub === "function") {
19
+ cleanup = unsub;
20
+ }
21
+ return () => {
22
+ cleanup?.();
23
+ };
24
+ });
25
+
26
+ return state;
27
+ }
@@ -0,0 +1,28 @@
1
+ import { writable } from "svelte/store";
2
+ import { onMount } from "svelte";
3
+ import { getMutationState, onMutationStateChange } from "../mutation/state";
4
+ import type { MutationState } from "../mutation/state";
5
+
6
+ export function useSwoffMutationState(id: string | null) {
7
+ const state = writable<MutationState | null>(null);
8
+
9
+ function update() {
10
+ state.set(id ? (getMutationState(id) ?? null) : null);
11
+ }
12
+
13
+ onMount(update);
14
+
15
+ let cleanup: (() => void) | undefined;
16
+
17
+ onMount(() => {
18
+ const unsub = onMutationStateChange(update);
19
+ if (typeof unsub === "function") {
20
+ cleanup = unsub;
21
+ }
22
+ return () => {
23
+ cleanup?.();
24
+ };
25
+ });
26
+
27
+ return state;
28
+ }