@swoff/cli 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -326
- package/bin/swoff +8 -14
- package/dist/__tests__/assemble-sw.test.js +6 -6
- package/dist/__tests__/assemble-sw.test.js.map +1 -1
- package/dist/__tests__/cli-integration.test.js +0 -1
- package/dist/__tests__/cli-integration.test.js.map +1 -1
- package/dist/__tests__/config-types.test.js +9 -5
- package/dist/__tests__/config-types.test.js.map +1 -1
- package/dist/__tests__/file-generators.test.js +12 -23
- package/dist/__tests__/file-generators.test.js.map +1 -1
- package/dist/__tests__/loader.test.js +0 -1
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/validator.test.js +50 -2
- package/dist/__tests__/validator.test.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/add.js +7 -6
- package/dist/lib/commands/add.js.map +1 -1
- package/dist/lib/commands/generate-guide.js +36 -9
- package/dist/lib/commands/generate-guide.js.map +1 -1
- package/dist/lib/commands/info.js +45 -8
- package/dist/lib/commands/info.js.map +1 -1
- package/dist/lib/commands/init.js +6 -2
- package/dist/lib/commands/init.js.map +1 -1
- package/dist/lib/config/validator.js +85 -4
- package/dist/lib/config/validator.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-fetch.js +3 -3
- package/dist/lib/generators/file-generators/auth-fetch.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-store.js +114 -5
- package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-user.js +2 -2
- package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
- package/dist/lib/generators/file-generators/cache.js +1 -3
- package/dist/lib/generators/file-generators/cache.js.map +1 -1
- package/dist/lib/generators/file-generators/client-injector.js +5 -17
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-wrapper.js +86 -20
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-hooks.js +28 -210
- package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
- package/dist/lib/generators/file-generators/gql-wrapper.js +152 -0
- package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -0
- package/dist/lib/generators/file-generators/guide-generator.js +357 -36
- package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-queue.js +130 -34
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-state.js +126 -0
- package/dist/lib/generators/file-generators/mutation-state.js.map +1 -0
- package/dist/lib/generators/file-generators/push.js +143 -0
- package/dist/lib/generators/file-generators/push.js.map +1 -0
- package/dist/lib/generators/file-generators/quick-readme.js +71 -11
- package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
- package/dist/lib/generators/file-generators/server-push.js +136 -0
- package/dist/lib/generators/file-generators/server-push.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-generator-build.js +1 -1
- package/dist/lib/generators/file-generators/sw-injector.js +1 -1
- package/dist/lib/generators/file-generators/sw-push.js +67 -0
- package/dist/lib/generators/file-generators/sw-push.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-template.js +2 -0
- package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
- package/dist/lib/generators/file-generators/type-definitions.js +39 -11
- package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
- package/dist/lib/generators/sw-generator.js +8 -9
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js +6 -2
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/assemble-sw.js +11 -2
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/background-sync-handler.js +22 -8
- package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/config-header.js +1 -1
- package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
- package/dist/lib/generators/sw-sections/default-template.js +2 -30
- package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
- package/dist/lib/generators/sw-sections/fetch-handler.js +230 -120
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/install-handler.js +12 -10
- package/dist/lib/generators/sw-sections/install-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/server-push-handler.js +114 -0
- package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/tag-management.js +17 -5
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +16 -10
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +44 -4
- package/dist/lib/shared/config-types.js.map +1 -1
- package/package.json +3 -2
- package/templates/hooks/useAuth.jsx +30 -0
- package/templates/hooks/useAuth.tsx +30 -0
- package/templates/hooks/useBackgroundSync.jsx +31 -0
- package/templates/hooks/useBackgroundSync.tsx +31 -0
- package/templates/hooks/useCacheInvalidation.jsx +19 -0
- package/templates/hooks/useCacheInvalidation.tsx +19 -0
- package/templates/hooks/useCachedFetch.jsx +94 -0
- package/templates/hooks/useCachedFetch.tsx +112 -0
- package/templates/hooks/useMutation.jsx +76 -0
- package/templates/hooks/useMutation.tsx +97 -0
- package/templates/hooks/useMutationQueue.jsx +42 -0
- package/templates/hooks/useMutationQueue.tsx +49 -0
- package/templates/hooks/useMutationState.jsx +27 -0
- package/templates/hooks/useMutationState.tsx +41 -0
- package/templates/hooks/useNetworkStatus.jsx +19 -0
- package/templates/hooks/useNetworkStatus.tsx +19 -0
- package/templates/hooks/usePrefetch.jsx +10 -0
- package/templates/hooks/usePrefetch.tsx +25 -0
- package/templates/hooks/usePushSubscription.jsx +67 -0
- package/templates/hooks/usePushSubscription.tsx +67 -0
- package/templates/hooks/useSWUpdate.jsx +76 -0
- package/templates/hooks/useSWUpdate.tsx +76 -0
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generates mutation-queue.js - offline mutation queue with IndexedDB.
|
|
3
|
-
*/
|
|
4
1
|
import { writeFile } from "./context.js";
|
|
5
2
|
export function generateMutationQueue(ctx) {
|
|
6
3
|
const ext = ctx.ext;
|
|
@@ -10,24 +7,32 @@ export function generateMutationQueue(ctx) {
|
|
|
10
7
|
const PT = (type) => (ts ? `<${type}>` : "");
|
|
11
8
|
const AS = (type) => (ts ? ` as ${type}` : "");
|
|
12
9
|
const authEnabled = ctx.config.features.auth.enabled;
|
|
10
|
+
const mqConfig = ctx.config.features.mutationQueue;
|
|
11
|
+
const batchSize = mqConfig.batchSize;
|
|
12
|
+
const batchDelayMs = mqConfig.batchDelayMs;
|
|
13
|
+
const maxRetries = mqConfig.maxRetries;
|
|
14
|
+
const retryBackoffMs = mqConfig.retryBackoffMs;
|
|
13
15
|
const importLines = authEnabled
|
|
14
16
|
? `import { getAuth } from "./auth/store.${ext}";
|
|
15
17
|
`
|
|
16
18
|
: "";
|
|
17
19
|
const additionalImports = `import { invalidateByTags } from "./cache.${ext}";
|
|
18
|
-
${ts
|
|
19
|
-
`
|
|
20
|
-
|
|
20
|
+
${ts
|
|
21
|
+
? `import type { MutationQueueItem } from "./swoff.d.ts";
|
|
22
|
+
`
|
|
23
|
+
: ""}`;
|
|
24
|
+
const authReplayHeaders = authEnabled
|
|
21
25
|
? ` const auth = await getAuth();
|
|
22
26
|
const authHeader${T("Record<string, string>")} = auth?.token ? { Authorization: \`Bearer \${auth.token}\` } : {};
|
|
23
27
|
`
|
|
24
28
|
: "";
|
|
25
|
-
const
|
|
26
|
-
? ` ...authHeader
|
|
27
|
-
: "";
|
|
29
|
+
const authHeaderSpread = authEnabled
|
|
30
|
+
? ` ...authHeader,\n`
|
|
31
|
+
: " ";
|
|
28
32
|
const code = `/**
|
|
29
33
|
* Swoff Mutation Queue
|
|
30
34
|
* Queue offline writes and sync when connection returns.
|
|
35
|
+
* Supports configurable batch size, rate limiting, and exponential backoff.
|
|
31
36
|
*
|
|
32
37
|
* Usage:
|
|
33
38
|
* import { queueMutation, processMutationQueue, flushMutations, getPendingCount } from './swoff/mutation-queue.${ext}';
|
|
@@ -44,11 +49,24 @@ ${ts ? `import type { MutationQueueItem } from "./swoff.d.ts";
|
|
|
44
49
|
* await flushMutations();
|
|
45
50
|
*
|
|
46
51
|
* // Auto-processes on online event
|
|
52
|
+
*
|
|
53
|
+
* Config:
|
|
54
|
+
* batchSize: ${batchSize} — mutations per progress event
|
|
55
|
+
* batchDelayMs: ${batchDelayMs} — delay between mutations (rate limiting)
|
|
56
|
+
* maxRetries: ${maxRetries} — max retries before dropping
|
|
57
|
+
* retryBackoffMs: ${retryBackoffMs} — exponential backoff base
|
|
47
58
|
*/
|
|
48
59
|
|
|
49
60
|
${importLines}${additionalImports}const DB_NAME = "swoff-queue";
|
|
50
61
|
const STORE_NAME = "mutations";
|
|
51
|
-
const
|
|
62
|
+
const BATCH_SIZE = ${batchSize};
|
|
63
|
+
const BATCH_DELAY_MS = ${batchDelayMs};
|
|
64
|
+
const MAX_RETRIES = ${maxRetries};
|
|
65
|
+
const RETRY_BACKOFF_MS = ${retryBackoffMs};
|
|
66
|
+
|
|
67
|
+
function sleep(ms${T("number")})${R("Promise<void>")}{
|
|
68
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
69
|
+
}
|
|
52
70
|
|
|
53
71
|
function openQueueDB()${R("Promise<IDBDatabase>")}{
|
|
54
72
|
return new Promise${PT("IDBDatabase")}((resolve, reject) => {
|
|
@@ -73,14 +91,27 @@ export async function queueMutation(mutation${T("Partial<MutationQueueItem>")})$
|
|
|
73
91
|
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
74
92
|
const store = tx.objectStore(STORE_NAME);
|
|
75
93
|
|
|
94
|
+
let body = mutation.body;
|
|
95
|
+
let bodyType = "json";
|
|
96
|
+
if (body instanceof FormData) {
|
|
97
|
+
bodyType = "formdata";
|
|
98
|
+
body = [...body.entries()];
|
|
99
|
+
} else if (body instanceof Blob) {
|
|
100
|
+
bodyType = "blob";
|
|
101
|
+
} else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
|
|
102
|
+
bodyType = "buffer";
|
|
103
|
+
}
|
|
104
|
+
|
|
76
105
|
store.add({
|
|
77
106
|
id: crypto.randomUUID(),
|
|
78
107
|
method: mutation.method,
|
|
79
108
|
url: mutation.url,
|
|
80
|
-
body
|
|
109
|
+
body,
|
|
110
|
+
bodyType,
|
|
81
111
|
headers: mutation.headers || {},
|
|
82
112
|
timestamp: Date.now(),
|
|
83
113
|
retryCount: 0,
|
|
114
|
+
nextRetryAt: 0,
|
|
84
115
|
tags: mutation.tags || [],
|
|
85
116
|
});
|
|
86
117
|
|
|
@@ -92,13 +123,57 @@ export async function queueMutation(mutation${T("Partial<MutationQueueItem>")})$
|
|
|
92
123
|
window.dispatchEvent(new CustomEvent("mutation-queue-changed"));
|
|
93
124
|
}
|
|
94
125
|
|
|
95
|
-
/**
|
|
126
|
+
/** Replay a single queued mutation. Returns true on success, false on failure. */
|
|
127
|
+
async function replayMutation(item${T("MutationQueueItem")})${R("Promise<boolean>")}{
|
|
128
|
+
try {
|
|
129
|
+
${authReplayHeaders} let replayBody${T("BodyInit | null")}${ts ? " = null" : ""};
|
|
130
|
+
let contentType${T("string | undefined")};
|
|
131
|
+
const bt = item.bodyType || "json";
|
|
132
|
+
if (bt === "formdata") {
|
|
133
|
+
replayBody = new FormData();
|
|
134
|
+
for (const [key, value] of (item.body || [])${AS("[string, FormDataEntryValue][]")}) {
|
|
135
|
+
replayBody.append(key, value);
|
|
136
|
+
}
|
|
137
|
+
} else if (bt === "blob") {
|
|
138
|
+
replayBody = item.body${AS("BodyInit | null")};
|
|
139
|
+
} else if (bt === "buffer") {
|
|
140
|
+
replayBody = item.body instanceof ArrayBuffer ? new Uint8Array(item.body)${AS("BodyInit")} : item.body${AS("BodyInit")};
|
|
141
|
+
} else {
|
|
142
|
+
replayBody = JSON.stringify(item.body);
|
|
143
|
+
contentType = "application/json";
|
|
144
|
+
}
|
|
145
|
+
const response = await fetch(item.url, {
|
|
146
|
+
method: item.method,
|
|
147
|
+
headers: {
|
|
148
|
+
...(contentType ? { "Content-Type": contentType } : {}),
|
|
149
|
+
${authHeaderSpread} ...item.headers,
|
|
150
|
+
},
|
|
151
|
+
body: replayBody,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!response.ok) throw new Error(\`HTTP \${response.status}\`);
|
|
155
|
+
|
|
156
|
+
if (item.tags && item.tags.length > 0) {
|
|
157
|
+
await invalidateByTags(item.tags);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await removeFromQueue(item.id);
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
item.retryCount++;
|
|
164
|
+
item.nextRetryAt = Date.now() + RETRY_BACKOFF_MS * Math.pow(2, item.retryCount - 1);
|
|
165
|
+
await updateInQueue(item);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Process all queued mutations in order. Sends them to the server. Runs automatically on the online event. Respects batchSize for progress reporting and batchDelayMs for rate limiting. */
|
|
96
171
|
export async function processMutationQueue()${R("Promise<void>")}{
|
|
97
172
|
if (!navigator.onLine || isSyncing) return;
|
|
98
173
|
isSyncing = true;
|
|
99
174
|
|
|
100
175
|
try {
|
|
101
|
-
|
|
176
|
+
const db = await openQueueDB();
|
|
102
177
|
const tx = db.transaction(STORE_NAME, "readonly");
|
|
103
178
|
const store = tx.objectStore(STORE_NAME);
|
|
104
179
|
const index = store.index("by-timestamp");
|
|
@@ -110,6 +185,7 @@ ${authReplayBlock} const db = await openQueueDB();
|
|
|
110
185
|
|
|
111
186
|
let succeeded = 0;
|
|
112
187
|
let failed = 0;
|
|
188
|
+
const total = queue.length;
|
|
113
189
|
|
|
114
190
|
for (const item of queue) {
|
|
115
191
|
if (item.retryCount >= MAX_RETRIES) {
|
|
@@ -118,34 +194,36 @@ ${authReplayBlock} const db = await openQueueDB();
|
|
|
118
194
|
continue;
|
|
119
195
|
}
|
|
120
196
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
"Content-Type": "application/json",
|
|
126
|
-
${authHeadersSpread} ...item.headers,
|
|
127
|
-
},
|
|
128
|
-
body: JSON.stringify(item.body),
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
if (!response.ok) throw new Error(\`HTTP \${response.status}\`);
|
|
132
|
-
|
|
133
|
-
if (item.tags && item.tags.length > 0) {
|
|
134
|
-
await invalidateByTags(item.tags);
|
|
135
|
-
}
|
|
197
|
+
// Skip items whose backoff delay hasn't elapsed yet
|
|
198
|
+
if (item.nextRetryAt && Date.now() < item.nextRetryAt) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
136
201
|
|
|
137
|
-
|
|
202
|
+
const ok = await replayMutation(item);
|
|
203
|
+
if (ok) {
|
|
138
204
|
succeeded++;
|
|
139
|
-
}
|
|
140
|
-
item.retryCount++;
|
|
141
|
-
await updateInQueue(item);
|
|
205
|
+
} else {
|
|
142
206
|
failed++;
|
|
143
207
|
}
|
|
208
|
+
|
|
209
|
+
// Rate limiting delay between mutations
|
|
210
|
+
if (BATCH_DELAY_MS > 0 && succeeded + (failed - succeeded) < total) {
|
|
211
|
+
await sleep(BATCH_DELAY_MS);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Emit progress after every BATCH_SIZE mutations
|
|
215
|
+
if ((succeeded + failed) % BATCH_SIZE === 0 || succeeded + failed === total) {
|
|
216
|
+
window.dispatchEvent(
|
|
217
|
+
new CustomEvent("mutation-sync-complete", {
|
|
218
|
+
detail: { succeeded, failed, total, current: succeeded + failed },
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
}
|
|
144
222
|
}
|
|
145
223
|
|
|
146
224
|
window.dispatchEvent(
|
|
147
225
|
new CustomEvent("mutation-sync-complete", {
|
|
148
|
-
detail: { succeeded, failed },
|
|
226
|
+
detail: { succeeded, failed, total },
|
|
149
227
|
})
|
|
150
228
|
);
|
|
151
229
|
} finally {
|
|
@@ -159,7 +237,6 @@ export async function flushMutations()${R("Promise<void>")}{
|
|
|
159
237
|
await processMutationQueue();
|
|
160
238
|
}
|
|
161
239
|
`;
|
|
162
|
-
// Helper functions appended outside the template string
|
|
163
240
|
const helpers = `
|
|
164
241
|
async function removeFromQueue(id${T("string")})${R("Promise<void>")}{
|
|
165
242
|
const db = await openQueueDB();
|
|
@@ -191,6 +268,25 @@ export async function getPendingCount()${R("Promise<number>")}{
|
|
|
191
268
|
request.onerror = () => reject((request${AS("IDBRequest")}).error);
|
|
192
269
|
});
|
|
193
270
|
}
|
|
271
|
+
|
|
272
|
+
/** Get the position of a mutation in the queue (0-based). Returns -1 if not found. */
|
|
273
|
+
export async function getQueuePosition(id${T("string")})${R("Promise<number>")}{
|
|
274
|
+
const items = await getQueueItems();
|
|
275
|
+
return items.findIndex((item) => item.id === id);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Get all pending queue items with their details. */
|
|
279
|
+
export async function getQueueItems()${R("Promise<MutationQueueItem[]>")}{
|
|
280
|
+
const db = await openQueueDB();
|
|
281
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
282
|
+
const store = tx.objectStore(STORE_NAME);
|
|
283
|
+
const index = store.index("by-timestamp");
|
|
284
|
+
return new Promise${PT("MutationQueueItem[]")}((resolve, reject) => {
|
|
285
|
+
const request = index.getAll();
|
|
286
|
+
request.onsuccess = () => resolve(request.result);
|
|
287
|
+
request.onerror = () => reject(request.error);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
194
290
|
`;
|
|
195
291
|
writeFile(ctx, `mutation-queue.${ext}`, code + helpers);
|
|
196
292
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutation-queue.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/mutation-queue.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mutation-queue.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/mutation-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates mutation-state.ts/js — per-mutation state tracking.
|
|
3
|
+
* Provides a lightweight store for tracking individual mutation lifecycle states,
|
|
4
|
+
* enabling useMutation-style hooks and fine-grained mutation status UI.
|
|
5
|
+
*/
|
|
6
|
+
import { writeFile } from "./context.js";
|
|
7
|
+
export function generateMutationState(ctx) {
|
|
8
|
+
const ext = ctx.ext;
|
|
9
|
+
const ts = ext === "ts";
|
|
10
|
+
const T = (type) => (ts ? `: ${type}` : "");
|
|
11
|
+
const R = (type) => (ts ? `: ${type}` : " ");
|
|
12
|
+
const code = `/**
|
|
13
|
+
* Swoff Mutation State
|
|
14
|
+
* Lightweight in-memory mutation state tracker.
|
|
15
|
+
* Enables per-mutation status UI (loading spinners, error states, success indicators).
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* import { trackMutation, getMutationState, clearMutationState } from './swoff/mutation-state.${ext}';
|
|
19
|
+
*
|
|
20
|
+
* // After queueing a mutation
|
|
21
|
+
* const id = "mutation-" + crypto.randomUUID();
|
|
22
|
+
* trackMutation(id, "pending");
|
|
23
|
+
*
|
|
24
|
+
* // Check state later
|
|
25
|
+
* const state = getMutationState(id);
|
|
26
|
+
* if (state?.status === "error") { showError(state.error); }
|
|
27
|
+
*
|
|
28
|
+
* // Clear when done
|
|
29
|
+
* clearMutationState(id);
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export type MutationStatus = "idle" | "pending" | "success" | "error";
|
|
33
|
+
|
|
34
|
+
export interface MutationState {
|
|
35
|
+
id${T("string")};
|
|
36
|
+
status${T("MutationStatus")};
|
|
37
|
+
error${T("Error | null")};
|
|
38
|
+
data${T("unknown")};
|
|
39
|
+
timestamp${T("number")};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mutations = new Map<string, MutationState>();
|
|
43
|
+
|
|
44
|
+
let listenerId = 0;
|
|
45
|
+
const listeners = new Map<number, (state: MutationState) => void>();
|
|
46
|
+
|
|
47
|
+
/** Track a mutation with the given ID and initial status. */
|
|
48
|
+
export function trackMutation(id${T("string")}, status${T("MutationStatus")} = "idle")${R("MutationState")}{
|
|
49
|
+
let state = mutations.get(id);
|
|
50
|
+
if (!state) {
|
|
51
|
+
state = { id, status, error: null, data: null, timestamp: Date.now() };
|
|
52
|
+
mutations.set(id, state);
|
|
53
|
+
} else {
|
|
54
|
+
state.status = status;
|
|
55
|
+
state.timestamp = Date.now();
|
|
56
|
+
}
|
|
57
|
+
notifyListeners(state);
|
|
58
|
+
window.dispatchEvent(new CustomEvent("mutation-state-changed", { detail: state }));
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Update an existing mutation's state. */
|
|
63
|
+
export function updateMutationState(id${T("string")}, partial${T("Partial<MutationState>")})${R("MutationState | null")}{
|
|
64
|
+
const state = mutations.get(id);
|
|
65
|
+
if (!state) return null;
|
|
66
|
+
Object.assign(state, partial);
|
|
67
|
+
state.timestamp = Date.now();
|
|
68
|
+
notifyListeners(state);
|
|
69
|
+
window.dispatchEvent(new CustomEvent("mutation-state-changed", { detail: state }));
|
|
70
|
+
return state;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Mark a mutation as successful with optional data. */
|
|
74
|
+
export function resolveMutation(id${T("string")}, data${T("unknown")} = null)${R("void")}{
|
|
75
|
+
updateMutationState(id, { status: "success", data, error: null });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Mark a mutation as failed with an error. */
|
|
79
|
+
export function rejectMutation(id${T("string")}, error${T("Error")})${R("void")}{
|
|
80
|
+
updateMutationState(id, { status: "error", error });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Get the current state of a mutation by ID. */
|
|
84
|
+
export function getMutationState(id${T("string")})${R("MutationState | undefined")}{
|
|
85
|
+
return mutations.get(id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Remove a mutation's state from the tracker. */
|
|
89
|
+
export function clearMutationState(id${T("string")})${R("void")}{
|
|
90
|
+
mutations.delete(id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Get all tracked mutations. */
|
|
94
|
+
export function getAllMutationStates()${R("MutationState[]")}{
|
|
95
|
+
return Array.from(mutations.values());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Get the count of mutations in a given status. */
|
|
99
|
+
export function getMutationCount(status${T("MutationStatus")})${R("number")}{
|
|
100
|
+
let count = 0;
|
|
101
|
+
for (const state of mutations.values()) {
|
|
102
|
+
if (state.status === status) count++;
|
|
103
|
+
}
|
|
104
|
+
return count;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Subscribe to mutation state changes. Returns an unsubscribe function. */
|
|
108
|
+
export function onMutationStateChange(callback${T("(state: MutationState) => void")})${R("() => void")}{
|
|
109
|
+
const id = ++listenerId;
|
|
110
|
+
listeners.set(id, callback);
|
|
111
|
+
return () => { listeners.delete(id); };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function notifyListeners(state${T("MutationState")})${R("void")}{
|
|
115
|
+
for (const cb of listeners.values()) {
|
|
116
|
+
try {
|
|
117
|
+
cb(state);
|
|
118
|
+
} catch {
|
|
119
|
+
// Handle listener errors silently
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
writeFile(ctx, `mutation-state.${ext}`, code);
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=mutation-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation-state.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/mutation-state.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,qBAAqB,CAAC,GAAqB;IACzD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IACpB,MAAM,EAAE,GAAG,GAAG,KAAK,IAAI,CAAC;IACxB,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpD,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAErD,MAAM,IAAI,GAAG;;;;;;mGAMoF,GAAG;;;;;;;;;;;;;;;;;MAiBhG,CAAC,CAAC,QAAQ,CAAC;UACP,CAAC,CAAC,gBAAgB,CAAC;SACpB,CAAC,CAAC,cAAc,CAAC;QAClB,CAAC,CAAC,SAAS,CAAC;aACP,CAAC,CAAC,QAAQ,CAAC;;;;;;;;;kCASU,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC,eAAe,CAAC;;;;;;;;;;;;;;;wCAelE,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,wBAAwB,CAAC,IAAI,CAAC,CAAC,sBAAsB,CAAC;;;;;;;;;;;oCAWnF,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC;;;;;mCAKrD,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;;;;;qCAK1C,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,2BAA2B,CAAC;;;;;uCAK3C,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;;;;;wCAKvB,CAAC,CAAC,iBAAiB,CAAC;;;;;yCAKnB,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;;;;;;;;;gDAS3B,CAAC,CAAC,gCAAgC,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC;;;;;;gCAMtE,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;;;;;;;;;CAS9D,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,kBAAkB,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates push.{js|ts} - push notification subscription management.
|
|
3
|
+
*/
|
|
4
|
+
import { writeFile } from "./context.js";
|
|
5
|
+
export function generatePush(ctx) {
|
|
6
|
+
const ext = ctx.ext;
|
|
7
|
+
const ts = ext === "ts";
|
|
8
|
+
const T = (type) => (ts ? `: ${type}` : "");
|
|
9
|
+
const R = (type) => (ts ? `: ${type} ` : " ");
|
|
10
|
+
const PT = (type) => (ts ? `<${type}>` : "");
|
|
11
|
+
const AS = (type) => (ts ? ` as ${type}` : "");
|
|
12
|
+
const code = `/**
|
|
13
|
+
* Swoff Push Notifications
|
|
14
|
+
* Push subscription management with IndexedDB persistence.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* import { subscribeToPush, unsubscribeFromPush, isSubscribed } from './swoff/push.${ext}';
|
|
18
|
+
*
|
|
19
|
+
* // Enable (triggers permission prompt)
|
|
20
|
+
* const subscription = await subscribeToPush("YOUR_VAPID_PUBLIC_KEY");
|
|
21
|
+
* await fetch("/api/push/subscribe", {
|
|
22
|
+
* method: "POST",
|
|
23
|
+
* body: JSON.stringify(subscription.toJSON()),
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Disable
|
|
27
|
+
* await unsubscribeFromPush();
|
|
28
|
+
*
|
|
29
|
+
* Window events:
|
|
30
|
+
* push-permission-changed - Permission granted/denied (detail: { permission })
|
|
31
|
+
* push-subscription-changed - Subscribed/unsubscribed (detail: { subscribed })
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const SUBSCRIPTION_DB = "swoff-push";
|
|
35
|
+
const SUBSCRIPTION_STORE = "subscription";
|
|
36
|
+
|
|
37
|
+
let permissionState = Notification.permission;
|
|
38
|
+
|
|
39
|
+
function openPushDB()${R("Promise<IDBDatabase>")}{
|
|
40
|
+
return new Promise${PT("IDBDatabase")}((resolve, reject) => {
|
|
41
|
+
const request = indexedDB.open(SUBSCRIPTION_DB, 1);
|
|
42
|
+
request.onupgradeneeded = (e) => {
|
|
43
|
+
const db = (e.target${AS("IDBOpenDBRequest")}).result;
|
|
44
|
+
if (!db.objectStoreNames.contains(SUBSCRIPTION_STORE)) {
|
|
45
|
+
db.createObjectStore(SUBSCRIPTION_STORE, { keyPath: "id" });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
request.onsuccess = (e) => resolve((e.target${AS("IDBOpenDBRequest")}).result);
|
|
49
|
+
request.onerror = (e) => reject((e.target${AS("IDBRequest")}).error);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Request notification permission from the user. Returns true if granted. */
|
|
54
|
+
export async function requestNotificationPermission()${R("Promise<boolean>")}{
|
|
55
|
+
if (permissionState === "granted") return true;
|
|
56
|
+
if (permissionState === "denied") return false;
|
|
57
|
+
|
|
58
|
+
const result = await Notification.requestPermission();
|
|
59
|
+
permissionState = result;
|
|
60
|
+
window.dispatchEvent(
|
|
61
|
+
new CustomEvent("push-permission-changed", { detail: { permission: result } }),
|
|
62
|
+
);
|
|
63
|
+
return result === "granted";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Get the current push subscription, or null if not subscribed. */
|
|
67
|
+
export async function getPushSubscription()${R("Promise<PushSubscription | null>")}{
|
|
68
|
+
try {
|
|
69
|
+
const registration = await navigator.serviceWorker.ready;
|
|
70
|
+
return await registration.pushManager.getSubscription();
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Subscribe to push notifications. Returns the subscription or null if permission denied. */
|
|
77
|
+
export async function subscribeToPush(vapidPublicKey${T("string")})${R("Promise<PushSubscription | null>")}{
|
|
78
|
+
const granted = await requestNotificationPermission();
|
|
79
|
+
if (!granted) return null;
|
|
80
|
+
|
|
81
|
+
const registration = await navigator.serviceWorker.ready;
|
|
82
|
+
const subscription = await registration.pushManager.subscribe({
|
|
83
|
+
userVisibleOnly: true,
|
|
84
|
+
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)${AS("BufferSource")},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const db = await openPushDB();
|
|
88
|
+
const tx = db.transaction(SUBSCRIPTION_STORE, "readwrite");
|
|
89
|
+
tx.objectStore(SUBSCRIPTION_STORE).put({
|
|
90
|
+
id: "current",
|
|
91
|
+
endpoint: subscription.endpoint,
|
|
92
|
+
keys: subscription.toJSON().keys,
|
|
93
|
+
subscribedAt: Date.now(),
|
|
94
|
+
});
|
|
95
|
+
await new Promise<void>((resolve, reject) => {
|
|
96
|
+
tx.oncomplete = () => resolve();
|
|
97
|
+
tx.onerror = () => reject(tx.error);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
window.dispatchEvent(
|
|
101
|
+
new CustomEvent("push-subscription-changed", { detail: { subscribed: true } }),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return subscription;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Unsubscribe from push notifications. */
|
|
108
|
+
export async function unsubscribeFromPush()${R("Promise<void>")}{
|
|
109
|
+
const subscription = await getPushSubscription();
|
|
110
|
+
if (!subscription) return;
|
|
111
|
+
|
|
112
|
+
await subscription.unsubscribe();
|
|
113
|
+
|
|
114
|
+
const db = await openPushDB();
|
|
115
|
+
const tx = db.transaction(SUBSCRIPTION_STORE, "readwrite");
|
|
116
|
+
tx.objectStore(SUBSCRIPTION_STORE).delete("current");
|
|
117
|
+
await new Promise<void>((resolve, reject) => {
|
|
118
|
+
tx.oncomplete = () => resolve();
|
|
119
|
+
tx.onerror = () => reject(tx.error);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
window.dispatchEvent(
|
|
123
|
+
new CustomEvent("push-subscription-changed", { detail: { subscribed: false } }),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Check if the user is subscribed to push notifications. */
|
|
128
|
+
export async function isSubscribed()${R("Promise<boolean>")}{
|
|
129
|
+
const sub = await getPushSubscription();
|
|
130
|
+
return sub !== null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Convert a base64 VAPID public key to a Uint8Array for pushManager.subscribe(). */
|
|
134
|
+
function urlBase64ToUint8Array(base64String${T("string")})${R("Uint8Array")}{
|
|
135
|
+
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
136
|
+
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
|
137
|
+
const rawData = atob(base64);
|
|
138
|
+
return Uint8Array.from(rawData, (c) => c.charCodeAt(0));
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
writeFile(ctx, `push.${ext}`, code);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/push.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,YAAY,CAAC,GAAqB;IAChD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IACpB,MAAM,EAAE,GAAG,GAAG,KAAK,IAAI,CAAC;IACxB,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpD,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACtD,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrD,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAEvD,MAAM,IAAI,GAAG;;;;;wFAKyE,GAAG;;;;;;;;;;;;;;;;;;;;;;uBAsBpE,CAAC,CAAC,sBAAsB,CAAC;sBAC1B,EAAE,CAAC,aAAa,CAAC;;;4BAGX,EAAE,CAAC,kBAAkB,CAAC;;;;;kDAKA,EAAE,CAAC,kBAAkB,CAAC;+CACzB,EAAE,CAAC,YAAY,CAAC;;;;;uDAKR,CAAC,CAAC,kBAAkB,CAAC;;;;;;;;;;;;;6CAa/B,CAAC,CAAC,kCAAkC,CAAC;;;;;;;;;;sDAU5B,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,kCAAkC,CAAC;;;;;;;iEAOzC,EAAE,CAAC,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;;;;;6CAwBtC,CAAC,CAAC,eAAe,CAAC;;;;;;;;;;;;;;;;;;;;sCAoBzB,CAAC,CAAC,kBAAkB,CAAC;;;;;;6CAMd,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC;;;;;;CAM1E,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,QAAQ,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AACtC,CAAC"}
|
|
@@ -11,7 +11,7 @@ export function generateReadme(ctx) {
|
|
|
11
11
|
w("");
|
|
12
12
|
w("For the full guide with explanations, read **[GUIDE.md](./GUIDE.md)**.");
|
|
13
13
|
w("");
|
|
14
|
-
const hasClientInjector =
|
|
14
|
+
const hasClientInjector = true;
|
|
15
15
|
if (hasClientInjector) {
|
|
16
16
|
w("## Entry point");
|
|
17
17
|
w("```ts");
|
|
@@ -21,6 +21,8 @@ export function generateReadme(ctx) {
|
|
|
21
21
|
w("");
|
|
22
22
|
}
|
|
23
23
|
w("## API calls (use for all fetch requests)");
|
|
24
|
+
w("Use `fetchWithCache` instead of `fetch()` — it sets caching headers the SW needs.");
|
|
25
|
+
w("Plain `fetch()` skips SW caching when `cacheStrategy` is `\"explicit-only\"`.");
|
|
24
26
|
w("```ts");
|
|
25
27
|
w(`import { fetchWithCache } from "./swoff/fetch-wrapper.${ext}";`);
|
|
26
28
|
w('const data = await fetchWithCache("/api/data").then(r => r.json());');
|
|
@@ -29,12 +31,12 @@ export function generateReadme(ctx) {
|
|
|
29
31
|
if (config.features.auth.enabled) {
|
|
30
32
|
w("## Authenticated API calls");
|
|
31
33
|
w("```ts");
|
|
32
|
-
w(`import {
|
|
33
|
-
w('const data = await
|
|
34
|
+
w(`import { fetchWithCache } from "./swoff/fetch-wrapper.${ext}";`);
|
|
35
|
+
w('const data = await fetchWithCache("/api/me", { auth: true }).then(r => r.json());');
|
|
34
36
|
w("```");
|
|
35
37
|
w("");
|
|
36
38
|
}
|
|
37
|
-
if (config.features.mutationQueue) {
|
|
39
|
+
if (config.features.mutationQueue.enabled) {
|
|
38
40
|
w("## Offline mutations (queue writes when offline)");
|
|
39
41
|
w("```ts");
|
|
40
42
|
w(`import { queueMutation, getPendingCount } from "./swoff/mutation-queue.${ext}";`);
|
|
@@ -50,20 +52,78 @@ export function generateReadme(ctx) {
|
|
|
50
52
|
w("");
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
|
-
if (config.features.
|
|
54
|
-
w("##
|
|
55
|
+
if (config.features.graphql.enabled) {
|
|
56
|
+
w("## GraphQL (cached queries with body-hash)");
|
|
55
57
|
w("```ts");
|
|
56
|
-
w(`import {
|
|
57
|
-
w('const data = await
|
|
58
|
-
w('
|
|
58
|
+
w(`import { queryGql, mutateGql } from "./swoff/gql-wrapper.${ext}";`);
|
|
59
|
+
w('const { data } = await queryGql("{ todos { id title } }");');
|
|
60
|
+
w('const { data: created } = await mutateGql(');
|
|
61
|
+
w(' "mutation CreateTodo($t: String!) { createTodo(title: $t) { id } }",');
|
|
62
|
+
w(' { t: "New task" },');
|
|
63
|
+
w(");");
|
|
64
|
+
w("```");
|
|
65
|
+
w("");
|
|
66
|
+
}
|
|
67
|
+
if (config.features.serverPush.enabled) {
|
|
68
|
+
w("## Server push events (real-time cache invalidation)");
|
|
69
|
+
w("```ts");
|
|
70
|
+
w(`import { startPushEvents } from "./swoff/server-push.${ext}";`);
|
|
71
|
+
w("startPushEvents();");
|
|
72
|
+
w("```");
|
|
73
|
+
w("");
|
|
74
|
+
}
|
|
75
|
+
if (config.features.pushNotifications?.enabled) {
|
|
76
|
+
w("## Push notifications");
|
|
77
|
+
w("```ts");
|
|
78
|
+
w(`import { subscribeToPush, unsubscribeFromPush, isSubscribed } from "./swoff/push.${ext}";`);
|
|
79
|
+
w('const sub = await subscribeToPush("YOUR_VAPID_PUBLIC_KEY");');
|
|
59
80
|
w("```");
|
|
60
81
|
w("");
|
|
61
82
|
}
|
|
62
83
|
if (ctx.frameworkName === "react") {
|
|
63
|
-
w("## React
|
|
84
|
+
w("## React hooks");
|
|
64
85
|
w("```tsx");
|
|
65
86
|
w(`import { useCachedFetch } from "./swoff/hooks/useCachedFetch.${ext}x";`);
|
|
66
|
-
w(
|
|
87
|
+
w(`import { useMutation } from "./swoff/hooks/useMutation.${ext}x";`);
|
|
88
|
+
w(`import { usePrefetch } from "./swoff/hooks/usePrefetch.${ext}x";`);
|
|
89
|
+
w(`import { useMutationState } from "./swoff/hooks/useMutationState.${ext}x";`);
|
|
90
|
+
w("");
|
|
91
|
+
w('const { data, error, loading, refetch } = useCachedFetch("/api/todos", {');
|
|
92
|
+
w(' refetchOnWindowFocus: true,');
|
|
93
|
+
w('});');
|
|
94
|
+
w("");
|
|
95
|
+
w('// Dependent query — skip until user is loaded');
|
|
96
|
+
w('const { data: posts } = useCachedFetch("/api/posts", { enabled: !!user });');
|
|
97
|
+
w("");
|
|
98
|
+
w('const { mutate } = useMutation({ onSuccess: (data) => console.log(data) });');
|
|
99
|
+
w('mutate("/api/todos", { method: "POST", body: JSON.stringify({ title: "New" }) });');
|
|
100
|
+
w("");
|
|
101
|
+
w('const prefetch = usePrefetch();');
|
|
102
|
+
w('prefetch("/api/todos");');
|
|
103
|
+
w("");
|
|
104
|
+
w('// Track a specific mutation by its returned id');
|
|
105
|
+
w('const mutation = useMutationState(mutationId);');
|
|
106
|
+
w("```");
|
|
107
|
+
w("");
|
|
108
|
+
if (config.features.mutationQueue.enabled) {
|
|
109
|
+
w("## Mutation queue hooks");
|
|
110
|
+
w("```tsx");
|
|
111
|
+
w(`import { useMutationQueue } from "./swoff/hooks/useMutationQueue.${ext}x";`);
|
|
112
|
+
w(`import { useMutationState } from "./swoff/hooks/useMutationState.${ext}x";`);
|
|
113
|
+
w("");
|
|
114
|
+
w('const { pending, items, lastSync } = useMutationQueue();');
|
|
115
|
+
w("// items: MutationQueueItem[] — each with id, url, method, status, retryCount");
|
|
116
|
+
w("// lastSync: { succeeded, failed } | null — result of the last sync attempt");
|
|
117
|
+
w("```");
|
|
118
|
+
w("");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (config.features.tagInvalidation) {
|
|
122
|
+
w("## Cache invalidation");
|
|
123
|
+
w("```ts");
|
|
124
|
+
w(`import { generateTags, invalidateUrl } from "./swoff/invalidation-tags.${ext}";`);
|
|
125
|
+
w('const data = await fetchWithCache("/api/todos", { tags: generateTags("/api/todos") });');
|
|
126
|
+
w('await invalidateUrl("/api/todos/42"); // after mutation');
|
|
67
127
|
w("```");
|
|
68
128
|
w("");
|
|
69
129
|
}
|