@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.
Files changed (109) hide show
  1. package/README.md +43 -326
  2. package/bin/swoff +8 -14
  3. package/dist/__tests__/assemble-sw.test.js +6 -6
  4. package/dist/__tests__/assemble-sw.test.js.map +1 -1
  5. package/dist/__tests__/cli-integration.test.js +0 -1
  6. package/dist/__tests__/cli-integration.test.js.map +1 -1
  7. package/dist/__tests__/config-types.test.js +9 -5
  8. package/dist/__tests__/config-types.test.js.map +1 -1
  9. package/dist/__tests__/file-generators.test.js +12 -23
  10. package/dist/__tests__/file-generators.test.js.map +1 -1
  11. package/dist/__tests__/loader.test.js +0 -1
  12. package/dist/__tests__/loader.test.js.map +1 -1
  13. package/dist/__tests__/validator.test.js +50 -2
  14. package/dist/__tests__/validator.test.js.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib/commands/add.js +7 -6
  18. package/dist/lib/commands/add.js.map +1 -1
  19. package/dist/lib/commands/generate-guide.js +36 -9
  20. package/dist/lib/commands/generate-guide.js.map +1 -1
  21. package/dist/lib/commands/info.js +45 -8
  22. package/dist/lib/commands/info.js.map +1 -1
  23. package/dist/lib/commands/init.js +6 -2
  24. package/dist/lib/commands/init.js.map +1 -1
  25. package/dist/lib/config/validator.js +85 -4
  26. package/dist/lib/config/validator.js.map +1 -1
  27. package/dist/lib/generators/file-generators/auth-fetch.js +3 -3
  28. package/dist/lib/generators/file-generators/auth-fetch.js.map +1 -1
  29. package/dist/lib/generators/file-generators/auth-store.js +114 -5
  30. package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
  31. package/dist/lib/generators/file-generators/auth-user.js +2 -2
  32. package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
  33. package/dist/lib/generators/file-generators/cache.js +1 -3
  34. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  35. package/dist/lib/generators/file-generators/client-injector.js +5 -17
  36. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  37. package/dist/lib/generators/file-generators/fetch-wrapper.js +86 -20
  38. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  39. package/dist/lib/generators/file-generators/generate-hooks.js +28 -210
  40. package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
  41. package/dist/lib/generators/file-generators/gql-wrapper.js +152 -0
  42. package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -0
  43. package/dist/lib/generators/file-generators/guide-generator.js +357 -36
  44. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
  45. package/dist/lib/generators/file-generators/mutation-queue.js +130 -34
  46. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  47. package/dist/lib/generators/file-generators/mutation-state.js +126 -0
  48. package/dist/lib/generators/file-generators/mutation-state.js.map +1 -0
  49. package/dist/lib/generators/file-generators/push.js +143 -0
  50. package/dist/lib/generators/file-generators/push.js.map +1 -0
  51. package/dist/lib/generators/file-generators/quick-readme.js +71 -11
  52. package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
  53. package/dist/lib/generators/file-generators/server-push.js +136 -0
  54. package/dist/lib/generators/file-generators/server-push.js.map +1 -0
  55. package/dist/lib/generators/file-generators/sw-generator-build.js +1 -1
  56. package/dist/lib/generators/file-generators/sw-injector.js +1 -1
  57. package/dist/lib/generators/file-generators/sw-push.js +67 -0
  58. package/dist/lib/generators/file-generators/sw-push.js.map +1 -0
  59. package/dist/lib/generators/file-generators/sw-template.js +2 -0
  60. package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
  61. package/dist/lib/generators/file-generators/type-definitions.js +39 -11
  62. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
  63. package/dist/lib/generators/sw-generator.js +8 -9
  64. package/dist/lib/generators/sw-generator.js.map +1 -1
  65. package/dist/lib/generators/sw-sections/activate-handler.js +6 -2
  66. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
  67. package/dist/lib/generators/sw-sections/assemble-sw.js +11 -2
  68. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  69. package/dist/lib/generators/sw-sections/background-sync-handler.js +22 -8
  70. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
  71. package/dist/lib/generators/sw-sections/config-header.js +1 -1
  72. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  73. package/dist/lib/generators/sw-sections/default-template.js +2 -30
  74. package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
  75. package/dist/lib/generators/sw-sections/fetch-handler.js +230 -120
  76. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  77. package/dist/lib/generators/sw-sections/install-handler.js +12 -10
  78. package/dist/lib/generators/sw-sections/install-handler.js.map +1 -1
  79. package/dist/lib/generators/sw-sections/server-push-handler.js +114 -0
  80. package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -0
  81. package/dist/lib/generators/sw-sections/tag-management.js +17 -5
  82. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  83. package/dist/lib/generators/swoff-files-generator.js +16 -10
  84. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  85. package/dist/lib/shared/config-types.js +44 -4
  86. package/dist/lib/shared/config-types.js.map +1 -1
  87. package/package.json +3 -2
  88. package/templates/hooks/useAuth.jsx +30 -0
  89. package/templates/hooks/useAuth.tsx +30 -0
  90. package/templates/hooks/useBackgroundSync.jsx +31 -0
  91. package/templates/hooks/useBackgroundSync.tsx +31 -0
  92. package/templates/hooks/useCacheInvalidation.jsx +19 -0
  93. package/templates/hooks/useCacheInvalidation.tsx +19 -0
  94. package/templates/hooks/useCachedFetch.jsx +94 -0
  95. package/templates/hooks/useCachedFetch.tsx +112 -0
  96. package/templates/hooks/useMutation.jsx +76 -0
  97. package/templates/hooks/useMutation.tsx +97 -0
  98. package/templates/hooks/useMutationQueue.jsx +42 -0
  99. package/templates/hooks/useMutationQueue.tsx +49 -0
  100. package/templates/hooks/useMutationState.jsx +27 -0
  101. package/templates/hooks/useMutationState.tsx +41 -0
  102. package/templates/hooks/useNetworkStatus.jsx +19 -0
  103. package/templates/hooks/useNetworkStatus.tsx +19 -0
  104. package/templates/hooks/usePrefetch.jsx +10 -0
  105. package/templates/hooks/usePrefetch.tsx +25 -0
  106. package/templates/hooks/usePushSubscription.jsx +67 -0
  107. package/templates/hooks/usePushSubscription.tsx +67 -0
  108. package/templates/hooks/useSWUpdate.jsx +76 -0
  109. 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 ? `import type { MutationQueueItem } from "./swoff.d.ts";
19
- ` : ""}`;
20
- const authReplayBlock = authEnabled
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 authHeadersSpread = authEnabled
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 MAX_RETRIES = 5;
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: mutation.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
- /** Process all queued mutations in order. Sends them to the server. Runs automatically on the online event. */
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
- ${authReplayBlock} const db = await openQueueDB();
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
- try {
122
- const response = await fetch(item.url, {
123
- method: item.method,
124
- headers: {
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
- await removeFromQueue(item.id);
202
+ const ok = await replayMutation(item);
203
+ if (ok) {
138
204
  succeeded++;
139
- } catch {
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;;GAEG;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,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;IACvD,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;IAErD,MAAM,WAAW,GAAG,WAAW;QAC7B,CAAC,CAAC,yCAAyC,GAAG;CACjD;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,iBAAiB,GAAG,6CAA6C,GAAG;EAC1E,EAAE,CAAC,CAAC,CAAC;CACN,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IAEP,MAAM,eAAe,GAAG,WAAW;QACjC,CAAC,CAAC;oBACc,CAAC,CAAC,wBAAwB,CAAC;CAC9C;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,iBAAiB,GAAG,WAAW;QACnC,CAAC,CAAC,4BAA4B;QAC9B,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,IAAI,GAAG;;;;;oHAKqG,GAAG;;;;;;;;;;;;;;;;EAgBrH,WAAW,GAAG,iBAAiB;;;;wBAIT,CAAC,CAAC,sBAAsB,CAAC;sBAC3B,EAAE,CAAC,aAAa,CAAC;;;4BAGX,EAAE,CAAC,kBAAkB,CAAC;;;;;;kDAMA,EAAE,CAAC,kBAAkB,CAAC;+CACzB,EAAE,CAAC,YAAY,CAAC;;;;;;;8CAOjB,CAAC,CAAC,4BAA4B,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;8CAyBrD,CAAC,CAAC,eAAe,CAAC;;;;;EAK9D,eAAe;;;;iBAIA,CAAC,CAAC,qBAAqB,CAAC,uBAAuB,EAAE,CAAC,qBAAqB,CAAC;;;;;;;;;;;;;;;;;;;;;EAqBvF,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wCAgCqB,CAAC,CAAC,eAAe,CAAC;;;CAGzD,CAAC;IAEA,wDAAwD;IACxD,MAAM,OAAO,GAAG;mCACiB,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;qBAI/C,EAAE,CAAC,MAAM,CAAC;;;;;;mCAMI,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;qBAI1D,EAAE,CAAC,MAAM,CAAC;;;;;;;yCAOU,CAAC,CAAC,iBAAiB,CAAC;;sBAEvC,EAAE,CAAC,QAAQ,CAAC;;;gDAGc,EAAE,CAAC,oBAAoB,CAAC;6CAC3B,EAAE,CAAC,YAAY,CAAC;;;CAG5D,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,kBAAkB,GAAG,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,CAAC;AAC1D,CAAC"}
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 = config.features.clientRegistration || config.features.pwa.enabled || config.features.crossTabSync;
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 { authenticatedFetch } from "./swoff/auth/fetch.${ext}";`);
33
- w('const data = await authenticatedFetch("/api/me").then(r => r.json());');
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.tagInvalidation) {
54
- w("## Cache invalidation");
55
+ if (config.features.graphql.enabled) {
56
+ w("## GraphQL (cached queries with body-hash)");
55
57
  w("```ts");
56
- w(`import { generateTags, invalidateUrl } from "./swoff/invalidation-tags.${ext}";`);
57
- w('const data = await fetchWithCache("/api/todos", { tags: generateTags("/api/todos") });');
58
- w('await invalidateUrl("/api/todos/42"); // after mutation');
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 hook — auto-refetch on cache invalidation");
84
+ w("## React hooks");
64
85
  w("```tsx");
65
86
  w(`import { useCachedFetch } from "./swoff/hooks/useCachedFetch.${ext}x";`);
66
- w('const { data, error, loading, refetch } = useCachedFetch("/api/todos");');
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
  }