@swoff/cli 0.3.7 → 0.3.9

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 (121) hide show
  1. package/README.md +53 -606
  2. package/dist/__tests__/assemble-sw.test.js +8 -38
  3. package/dist/__tests__/assemble-sw.test.js.map +1 -1
  4. package/dist/__tests__/cli-integration.test.js +119 -11
  5. package/dist/__tests__/cli-integration.test.js.map +1 -1
  6. package/dist/__tests__/config-types.test.js +38 -18
  7. package/dist/__tests__/config-types.test.js.map +1 -1
  8. package/dist/__tests__/file-generators.test.js +2 -2
  9. package/dist/__tests__/file-generators.test.js.map +1 -1
  10. package/dist/__tests__/glob-matcher.test.js +86 -0
  11. package/dist/__tests__/glob-matcher.test.js.map +1 -0
  12. package/dist/__tests__/invalidation-tags.test.js +207 -23
  13. package/dist/__tests__/invalidation-tags.test.js.map +1 -1
  14. package/dist/__tests__/loader.test.js +12 -12
  15. package/dist/__tests__/loader.test.js.map +1 -1
  16. package/dist/__tests__/validator.test.js +170 -74
  17. package/dist/__tests__/validator.test.js.map +1 -1
  18. package/dist/index.js +12 -11
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/cli/help.js +0 -5
  21. package/dist/lib/cli/help.js.map +1 -1
  22. package/dist/lib/commands/add.js +52 -33
  23. package/dist/lib/commands/add.js.map +1 -1
  24. package/dist/lib/commands/clean.js +1 -15
  25. package/dist/lib/commands/clean.js.map +1 -1
  26. package/dist/lib/commands/generate-guide.js +5 -5
  27. package/dist/lib/commands/generate-guide.js.map +1 -1
  28. package/dist/lib/commands/generate.js +7 -21
  29. package/dist/lib/commands/generate.js.map +1 -1
  30. package/dist/lib/commands/info.js +37 -12
  31. package/dist/lib/commands/info.js.map +1 -1
  32. package/dist/lib/commands/init.js +2 -10
  33. package/dist/lib/commands/init.js.map +1 -1
  34. package/dist/lib/commands/validate.js +3 -3
  35. package/dist/lib/commands/validate.js.map +1 -1
  36. package/dist/lib/config/loader.js +3 -6
  37. package/dist/lib/config/loader.js.map +1 -1
  38. package/dist/lib/config/validator.js +192 -42
  39. package/dist/lib/config/validator.js.map +1 -1
  40. package/dist/lib/generators/file-generators/cache.js +2 -10
  41. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  42. package/dist/lib/generators/file-generators/client-injector.js +51 -12
  43. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  44. package/dist/lib/generators/file-generators/fetch-wrapper.js +127 -58
  45. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  46. package/dist/lib/generators/file-generators/generate-hooks.js +6 -2
  47. package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
  48. package/dist/lib/generators/file-generators/gql-wrapper.js +139 -0
  49. package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -0
  50. package/dist/lib/generators/file-generators/guide-generator.js +6 -436
  51. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
  52. package/dist/lib/generators/file-generators/invalidation-tags.js +206 -19
  53. package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
  54. package/dist/lib/generators/file-generators/manifest.js +2 -2
  55. package/dist/lib/generators/file-generators/manifest.js.map +1 -1
  56. package/dist/lib/generators/file-generators/mutation-queue.js +140 -52
  57. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  58. package/dist/lib/generators/file-generators/mutation-state.js +126 -0
  59. package/dist/lib/generators/file-generators/mutation-state.js.map +1 -0
  60. package/dist/lib/generators/file-generators/push.js +6 -7
  61. package/dist/lib/generators/file-generators/push.js.map +1 -1
  62. package/dist/lib/generators/file-generators/quick-readme.js +8 -75
  63. package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
  64. package/dist/lib/generators/file-generators/server-push.js +146 -0
  65. package/dist/lib/generators/file-generators/server-push.js.map +1 -0
  66. package/dist/lib/generators/file-generators/sw-generator-build.js +8 -9
  67. package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
  68. package/dist/lib/generators/file-generators/sw-injector.js +2 -1
  69. package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
  70. package/dist/lib/generators/file-generators/type-definitions.js +26 -1
  71. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
  72. package/dist/lib/generators/sw-generator.js +8 -18
  73. package/dist/lib/generators/sw-generator.js.map +1 -1
  74. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
  75. package/dist/lib/generators/sw-sections/assemble-sw.js +19 -11
  76. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  77. package/dist/lib/generators/sw-sections/background-sync-handler.js +80 -24
  78. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
  79. package/dist/lib/generators/sw-sections/config-header.js +2 -2
  80. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  81. package/dist/lib/generators/sw-sections/default-template.js +2 -1
  82. package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
  83. package/dist/lib/generators/sw-sections/fetch-handler.js +461 -121
  84. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  85. package/dist/lib/generators/sw-sections/message-handler.js +55 -15
  86. package/dist/lib/generators/sw-sections/message-handler.js.map +1 -1
  87. package/dist/lib/generators/sw-sections/server-push-handler.js +114 -0
  88. package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -0
  89. package/dist/lib/generators/sw-sections/sw-push.js +67 -0
  90. package/dist/lib/generators/sw-sections/sw-push.js.map +1 -0
  91. package/dist/lib/generators/sw-sections/tag-management.js +62 -21
  92. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  93. package/dist/lib/generators/swoff-files-generator.js +12 -16
  94. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  95. package/dist/lib/shared/config-types.js +133 -45
  96. package/dist/lib/shared/config-types.js.map +1 -1
  97. package/dist/lib/shared/glob-matcher.js +77 -0
  98. package/dist/lib/shared/glob-matcher.js.map +1 -0
  99. package/dist/lib/utils/tty-status.js +21 -0
  100. package/dist/lib/utils/tty-status.js.map +1 -0
  101. package/docs/API.md +565 -0
  102. package/docs/ARCHITECTURE.md +374 -0
  103. package/docs/CLI.md +148 -0
  104. package/docs/COMPARISON.md +192 -0
  105. package/docs/CONFIG.md +277 -0
  106. package/docs/ECOSYSTEM.md +33 -0
  107. package/package.json +2 -1
  108. package/templates/hooks/useAuth.jsx +2 -2
  109. package/templates/hooks/useAuth.tsx +2 -2
  110. package/templates/hooks/useCachedFetch.jsx +21 -7
  111. package/templates/hooks/useCachedFetch.tsx +23 -11
  112. package/templates/hooks/useMutation.jsx +76 -0
  113. package/templates/hooks/useMutation.tsx +109 -0
  114. package/templates/hooks/useMutationQueue.jsx +20 -11
  115. package/templates/hooks/useMutationQueue.tsx +31 -15
  116. package/templates/hooks/useMutationState.jsx +13 -0
  117. package/templates/hooks/useMutationState.tsx +25 -0
  118. package/templates/hooks/usePrefetch.jsx +10 -0
  119. package/templates/hooks/usePrefetch.tsx +25 -0
  120. package/templates/hooks/usePushSubscription.jsx +3 -3
  121. package/templates/hooks/usePushSubscription.tsx +3 -3
@@ -1,15 +1,114 @@
1
1
  /**
2
- * Generates invalidation-tags.js - URL-based tag generation helper.
2
+ * Generates invalidation-tags.{js|ts} pattern-based tag generation with
3
+ * configurable glob patterns, prefix skipping, singularization, and cascading.
3
4
  */
4
5
  import { writeFile } from "./context.js";
6
+ function escapeRegex(str) {
7
+ return str.replace(/[.+^${}()|[\]\\]/g, "\\$&");
8
+ }
9
+ function compilePatternEntry(rawPattern, tagTemplates) {
10
+ const rawParts = rawPattern.split("/");
11
+ const parts = rawParts.filter((p) => p !== "");
12
+ const paramNames = [];
13
+ const regexParts = [];
14
+ for (let i = 0; i < parts.length; i++) {
15
+ const part = parts[i];
16
+ if (part === "**") {
17
+ regexParts.push("(?:\\/[^/]+)*");
18
+ continue;
19
+ }
20
+ if (regexParts.length > 0) {
21
+ regexParts.push("\\/");
22
+ }
23
+ if (part === "*") {
24
+ regexParts.push("[^/]+");
25
+ }
26
+ else if (part.startsWith(":")) {
27
+ paramNames.push(part.slice(1));
28
+ regexParts.push("([^/]+)");
29
+ }
30
+ else if (part.includes("{")) {
31
+ const close = part.indexOf("}");
32
+ if (close !== -1) {
33
+ const inner = part.slice(part.indexOf("{") + 1, close);
34
+ const alternatives = inner.split(",").map((s) => escapeRegex(s.trim()));
35
+ regexParts.push("(?:" + alternatives.join("|") + ")");
36
+ }
37
+ else {
38
+ regexParts.push(escapeRegex(part));
39
+ }
40
+ }
41
+ else {
42
+ regexParts.push(escapeRegex(part));
43
+ }
44
+ }
45
+ if (regexParts.length === 0)
46
+ return null;
47
+ const hasLeadingSlash = rawPattern.startsWith("/");
48
+ const regex = (hasLeadingSlash ? "^\\/" : "^") + regexParts.join("") + "$";
49
+ // Validate template params match captured params
50
+ const templatePlaceholders = new Set();
51
+ for (const tmpl of tagTemplates) {
52
+ const matches = tmpl.match(/\{(\w+)\}/g);
53
+ if (matches) {
54
+ for (const m of matches) {
55
+ templatePlaceholders.add(m.slice(1, -1));
56
+ }
57
+ }
58
+ }
59
+ for (const ph of templatePlaceholders) {
60
+ if (!paramNames.includes(ph)) {
61
+ return null;
62
+ }
63
+ }
64
+ return { regex, params: paramNames, templates: tagTemplates };
65
+ }
66
+ function generatePatternCode(patterns) {
67
+ const entries = [];
68
+ for (const [pattern, templates] of Object.entries(patterns)) {
69
+ const compiled = compilePatternEntry(pattern, templates);
70
+ if (compiled) {
71
+ entries.push(` { re: new RegExp("${compiled.regex.replace(/\\/g, "\\\\")}"), params: ${JSON.stringify(compiled.params)}, templates: ${JSON.stringify(compiled.templates)} }`);
72
+ }
73
+ }
74
+ return "[\n" + entries.join(",\n") + "\n]";
75
+ }
76
+ function generateSingularizationCode(singularization) {
77
+ if (!singularization || Object.keys(singularization).length === 0)
78
+ return "null";
79
+ return JSON.stringify(singularization);
80
+ }
81
+ function generateCascadingCode(cascading) {
82
+ if (!cascading || Object.keys(cascading).length === 0)
83
+ return "null";
84
+ return JSON.stringify(cascading);
85
+ }
5
86
  export function generateInvalidationTags(ctx) {
6
87
  const ext = ctx.ext;
7
88
  const ts = ext === "ts";
8
89
  const T = (type) => (ts ? `: ${type}` : "");
9
90
  const R = (type) => (ts ? `: ${type} ` : " ");
91
+ const tiConfig = ctx.config.features.tagInvalidation;
92
+ const prefixes = tiConfig.prefixes ?? [
93
+ "api",
94
+ "v1",
95
+ "v2",
96
+ "v3",
97
+ "rest",
98
+ "graphql",
99
+ "gql",
100
+ ];
101
+ const patterns = tiConfig.patterns ?? {};
102
+ const singularization = tiConfig.singularization ?? {};
103
+ const cascading = tiConfig.cascading ?? {};
104
+ const patternCode = generatePatternCode(patterns);
105
+ const singularizationCode = generateSingularizationCode(singularization);
106
+ const cascadingCode = generateCascadingCode(cascading);
107
+ const prefixesCode = JSON.stringify(prefixes);
10
108
  const code = `/**
11
109
  * Swoff Invalidation Tags Helper
12
- * URL-based tag generation from REST endpoints for automatic cache invalidation.
110
+ * Pattern-based tag generation from URLs with configurable glob patterns,
111
+ * prefix skipping, singularization, and cascading invalidation.
13
112
  *
14
113
  * Usage:
15
114
  * import { generateTags, invalidateUrl } from './swoff/invalidation-tags.${ext}';
@@ -17,7 +116,6 @@ export function generateInvalidationTags(ctx) {
17
116
  * // Generate tags from URL
18
117
  * generateTags("/api/todos"); // ["todos"]
19
118
  * generateTags("/api/todos/42"); // ["todos", "todo:42"]
20
- * generateTags("/api/todos/42/comments"); // ["todos", "todo:42", "comments"]
21
119
  *
22
120
  * // Use with fetch wrapper
23
121
  * const data = await fetchWithCache("/api/todos", {
@@ -29,17 +127,50 @@ export function generateInvalidationTags(ctx) {
29
127
  */
30
128
 
31
129
  import { invalidateByTags } from "./cache.${ext}";
130
+ ${ts ? `
131
+ interface TagPattern {
132
+ re: RegExp;
133
+ params: string[];
134
+ templates: string[];
135
+ }
136
+ ` : ""}
137
+ // Pattern entries compiled from swoff.config.json
138
+ const TAG_PATTERNS${T("TagPattern[]")} = ${patternCode};
139
+
140
+ // URL path prefixes to skip during tag generation
141
+ const SKIP_PREFIXES = ${prefixesCode};
142
+
143
+ // Custom singularization map (plural → singular)
144
+ const SINGULARIZATION${T("Record<string, string> | null")} = ${singularizationCode};
32
145
 
33
- /** Generate cache invalidation tags from a URL path. e.g. /api/todos/42 ["todos", "todo:42"]. Used with fetchWithCache's tags option. */
146
+ // Cascading invalidation map (tagdependent tags)
147
+ const CASCADING${T("Record<string, string[]> | null")} = ${cascadingCode};
148
+
149
+ /** Generate cache invalidation tags from a URL path. Tries configured patterns first, falls back to segment-based generation. */
34
150
  export function generateTags(url${T("string | URL")})${R("string[]")}{
35
151
  const parsed = typeof url === "string" ? new URL(url, window.location.origin) : url;
36
- const segments = parsed.pathname.split("/").filter(Boolean);
152
+ const path = parsed.pathname;
153
+
154
+ // Try configured patterns first
155
+ for (const entry of TAG_PATTERNS) {
156
+ const match = path.match(entry.re);
157
+ if (match) {
158
+ const params${T("Record<string, string>")} = {};
159
+ for (let i = 0; i < entry.params.length; i++) {
160
+ params[entry.params[i]] = match[i + 1];
161
+ }
162
+ return entry.templates.map((tmpl) => {
163
+ return tmpl.replace(/\\{(\\w+)\\}/g, (_, key) => params[key] ?? "");
164
+ });
165
+ }
166
+ }
37
167
 
168
+ // Fallback: segment-based generation
169
+ const segments = path.split("/").filter(Boolean);
38
170
  if (segments.length === 0) return ["root"];
39
171
 
40
- const skipPrefixes = ["api", "v1", "v2", "v3", "rest", "graphql", "gql"];
41
172
  let startIdx = 0;
42
- while (startIdx < segments.length && skipPrefixes.includes(segments[startIdx])) {
173
+ while (startIdx < segments.length && SKIP_PREFIXES.includes(segments[startIdx])) {
43
174
  startIdx++;
44
175
  }
45
176
 
@@ -47,13 +178,14 @@ export function generateTags(url${T("string | URL")})${R("string[]")}{
47
178
  if (resourceSegments.length === 0) return ["root"];
48
179
 
49
180
  const tags = [];
50
-
51
181
  tags.push(resourceSegments[0]);
52
182
 
53
183
  if (resourceSegments.length >= 2 && !isNaN(Number(resourceSegments[1]))) {
54
184
  const collection = resourceSegments[0];
55
185
  const id = resourceSegments[1];
56
- const singular = collection.replace(/s$/, "");
186
+ const singular = SINGULARIZATION && SINGULARIZATION[collection] !== undefined
187
+ ? SINGULARIZATION[collection]
188
+ : collection.replace(/s$/, "");
57
189
  tags.push(\`\${singular}:\${id}\`);
58
190
  }
59
191
 
@@ -66,27 +198,82 @@ export function generateTags(url${T("string | URL")})${R("string[]")}{
66
198
  return tags;
67
199
  }
68
200
 
69
- /** Generate tags prefixed by HTTP method. e.g. POST /api/todos → ["post-todos"]. Useful for invalidating only mutation-tagged caches. */
201
+ /** Generate tags prefixed by HTTP method. e.g. POST /api/todos → ["post-todos"]. */
70
202
  export function generateTagsFromMethod(method${T("string")}, url${T("string | URL")})${R("string[]")}{
71
203
  const tags = generateTags(url);
72
-
73
- if (method === "GET" || method === "HEAD") {
74
- return tags;
75
- }
76
-
204
+ if (method === "GET" || method === "HEAD") return tags;
77
205
  return tags.map((tag) => \`\${method.toLowerCase()}-\${tag}\`);
78
206
  }
79
207
 
80
- /** Invalidate cached responses related to a URL (extracts tags from the URL then calls invalidateByTags). */
208
+ /** Invalidate cached responses related to a URL, including cascading dependencies. */
81
209
  export async function invalidateUrl(url${T("string | URL")})${R("Promise<void>")}{
82
210
  const tags = generateTags(url);
83
- await invalidateByTags(tags);
211
+ const allTags = CASCADING ? expandCascading(tags) : tags;
212
+ await invalidateByTags(allTags);
84
213
  }
85
214
 
86
- /** Invalidate cached responses tagged with method-prefixed tags from a URL. */
215
+ /** Invalidate cached responses tagged with method-prefixed tags, including cascading dependencies. */
87
216
  export async function invalidateByMethod(method${T("string")}, url${T("string | URL")})${R("Promise<void>")}{
88
217
  const tags = generateTagsFromMethod(method, url);
89
- await invalidateByTags(tags);
218
+ const allTags = CASCADING ? expandCascading(tags) : tags;
219
+ await invalidateByTags(allTags);
220
+ }
221
+
222
+ /** Expand tags with their cascading dependencies, deduplicated. */
223
+ export function expandCascading(tags${T("string[]")})${R("string[]")}{
224
+ if (!CASCADING) return [...tags];
225
+ const result = new Set(tags);
226
+ for (const tag of tags) {
227
+ const deps = CASCADING[tag];
228
+ if (deps) {
229
+ for (const dep of deps) {
230
+ result.add(dep);
231
+ }
232
+ }
233
+ }
234
+ return [...result];
235
+ }
236
+
237
+ /** Introspect: get all URLs cached under a given tag. */
238
+ export async function getUrlsForTag(tag${T("string")})${R("Promise<{ url: string; actualUrl: string }[]>")}{
239
+ const controller = navigator.serviceWorker?.controller;
240
+ if (!controller) return [];
241
+ return new Promise((resolve) => {
242
+ const channel = new MessageChannel();
243
+ channel.port1.onmessage = (event) => {
244
+ resolve(event.data.urls || []);
245
+ };
246
+ controller.postMessage(
247
+ { type: "GET_URLS_FOR_TAG", tag },
248
+ [channel.port2],
249
+ );
250
+ });
251
+ }
252
+
253
+ /** Introspect: get all tags associated with a given URL. */
254
+ export async function getTagsForUrl(url${T("string")})${R("Promise<string[]>")}{
255
+ const controller = navigator.serviceWorker?.controller;
256
+ if (!controller) return [];
257
+ return new Promise((resolve) => {
258
+ const channel = new MessageChannel();
259
+ channel.port1.onmessage = (event) => {
260
+ resolve(event.data.tags || []);
261
+ };
262
+ controller.postMessage(
263
+ { type: "GET_TAGS_FOR_URL", url },
264
+ [channel.port2],
265
+ );
266
+ });
267
+ }
268
+
269
+ /** Invalidate all cached responses whose URL matches the given glob pattern. */
270
+ export async function invalidateMatching(glob${T("string")})${R("Promise<void>")}{
271
+ const controller = navigator.serviceWorker?.controller;
272
+ if (!controller) return;
273
+ controller.postMessage({
274
+ type: "INVALIDATE_MATCHING",
275
+ glob,
276
+ });
90
277
  }
91
278
  `;
92
279
  writeFile(ctx, `invalidation-tags.${ext}`, code);
@@ -1 +1 @@
1
- {"version":3,"file":"invalidation-tags.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/invalidation-tags.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,wBAAwB,CAAC,GAAqB;IAC5D,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;IAEtD,MAAM,IAAI,GAAG;;;;;8EAK+D,GAAG;;;;;;;;;;;;;;;;4CAgBrC,GAAG;;;kCAGb,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+CAoCrB,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC;;;;;;;;;;;yCAW3D,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;;;iDAM/B,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;CAI1G,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,qBAAqB,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AACnD,CAAC"}
1
+ {"version":3,"file":"invalidation-tags.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/invalidation-tags.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAQ3D,SAAS,WAAW,CAAC,GAAW;IAC9B,OAAO,GAAG,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED,SAAS,mBAAmB,CAC1B,UAAkB,EAClB,YAAsB;IAEtB,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAEtB,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,UAAU,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YACjC,SAAS;QACX,CAAC;QAED,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;QAED,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjB,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;aAAM,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAChC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/B,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;aAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;gBACjB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;gBACvD,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBACxE,UAAU,CAAC,IAAI,CAAC,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;YACxD,CAAC;iBAAM,CAAC;gBACN,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,eAAe,GAAG,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;IAE3E,iDAAiD;IACjD,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/C,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QACzC,IAAI,OAAO,EAAE,CAAC;YACZ,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;gBACxB,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;IACH,CAAC;IACD,KAAK,MAAM,EAAE,IAAI,oBAAoB,EAAE,CAAC;QACtC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;AAChE,CAAC;AAED,SAAS,mBAAmB,CAAC,QAAkC;IAC7D,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QACzD,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CACV,uBAAuB,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,eAAe,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CACjK,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;AAC7C,CAAC;AAED,SAAS,2BAA2B,CAClC,eAAuC;IAEvC,IAAI,CAAC,eAAe,IAAI,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,KAAK,CAAC;QAC/D,OAAO,MAAM,CAAC;IAChB,OAAO,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,qBAAqB,CAAC,SAAmC;IAChE,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IACrE,OAAO,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,GAAqB;IAC5D,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;IAEtD,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC;IAErD,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,IAAI;QACpC,KAAK;QACL,IAAI;QACJ,IAAI;QACJ,IAAI;QACJ,MAAM;QACN,SAAS;QACT,KAAK;KACN,CAAC;IACF,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,IAAI,EAAE,CAAC;IACzC,MAAM,eAAe,GAAG,QAAQ,CAAC,eAAe,IAAI,EAAE,CAAC;IACvD,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAS,IAAI,EAAE,CAAC;IAE3C,MAAM,WAAW,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,mBAAmB,GAAG,2BAA2B,CAAC,eAAe,CAAC,CAAC;IACzE,MAAM,aAAa,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;IACvD,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAE9C,MAAM,IAAI,GAAG;;;;;;8EAM+D,GAAG;;;;;;;;;;;;;;;4CAerC,GAAG;EAC7C,EAAE,CAAC,CAAC,CAAC;;;;;;CAMN,CAAC,CAAC,CAAC,EAAE;;oBAEc,CAAC,CAAC,cAAc,CAAC,MAAM,WAAW;;;wBAG9B,YAAY;;;uBAGb,CAAC,CAAC,+BAA+B,CAAC,MAAM,mBAAmB;;;iBAGjE,CAAC,CAAC,iCAAiC,CAAC,MAAM,aAAa;;;kCAGtC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC;;;;;;;;oBAQhD,CAAC,CAAC,wBAAwB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+CA4CA,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC;;;;;;;yCAO3D,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;;;;iDAO/B,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;;;;sCAOrE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC;;;;;;;;;;;;;;;yCAe3B,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,+CAA+C,CAAC;;;;;;;;;;;;;;;;yCAgBjE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,mBAAmB,CAAC;;;;;;;;;;;;;;;;+CAgB/B,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;;;;;CAQ/E,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,qBAAqB,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AACnD,CAAC"}
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Generates manifest.json - PWA web app manifest.
3
3
  */
4
+ import { ensureDir } from "./context.js";
4
5
  import { existsSync, writeFileSync } from "fs";
5
6
  import { join } from "path";
6
7
  export function generateManifest(ctx) {
7
8
  const outputDir = join(ctx.projectRoot, "public");
8
- if (!existsSync(outputDir))
9
- return;
9
+ ensureDir(outputDir);
10
10
  const manifestPath = join(outputDir, "manifest.json");
11
11
  if (existsSync(manifestPath))
12
12
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/manifest.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,MAAM,UAAU,gBAAgB,CAAC,GAAqB;IACpD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAClD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO;IAEnC,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IACtD,IAAI,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO;IAErC,MAAM,QAAQ,GAAG;QACf,IAAI,EAAE,WAAW;QACjB,UAAU,EAAE,OAAO;QACnB,WAAW,EAAE,+BAA+B;QAC5C,SAAS,EAAE,GAAG;QACd,OAAO,EAAE,YAAY;QACrB,gBAAgB,EAAE,SAAS;QAC3B,WAAW,EAAE,SAAS;QACtB,WAAW,EAAE,kBAAkB;QAC/B,KAAK,EAAE,GAAG;QACV,IAAI,EAAE,OAAO;QACb,UAAU,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC;QAChC,2BAA2B,EAAE,KAAK;QAClC,gBAAgB,EAAE,CAAC,yBAAyB,EAAE,YAAY,EAAE,SAAS,CAAC;QACtE,KAAK,EAAE;YACL,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE;YAC7E,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE;SACvF;QACD,WAAW,EAAE,EAAE;KAChB,CAAC;IAEF,aAAa,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/D,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;AAClD,CAAC"}
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/manifest.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,MAAM,UAAU,gBAAgB,CAAC,GAAqB;IACpD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAClD,SAAS,CAAC,SAAS,CAAC,CAAC;IAErB,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IACtD,IAAI,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO;IAErC,MAAM,QAAQ,GAAG;QACf,IAAI,EAAE,WAAW;QACjB,UAAU,EAAE,OAAO;QACnB,WAAW,EAAE,+BAA+B;QAC5C,SAAS,EAAE,GAAG;QACd,OAAO,EAAE,YAAY;QACrB,gBAAgB,EAAE,SAAS;QAC3B,WAAW,EAAE,SAAS;QACtB,WAAW,EAAE,kBAAkB;QAC/B,KAAK,EAAE,GAAG;QACV,IAAI,EAAE,OAAO;QACb,UAAU,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC;QAChC,2BAA2B,EAAE,KAAK;QAClC,gBAAgB,EAAE,CAAC,yBAAyB,EAAE,YAAY,EAAE,SAAS,CAAC;QACtE,KAAK,EAAE;YACL,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE;YAC7E,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE;SACvF;QACD,WAAW,EAAE,EAAE;KAChB,CAAC;IAEF,aAAa,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/D,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;AAClD,CAAC"}
@@ -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,36 @@ 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 tagInvalidation = ctx.config.features.tagInvalidation.enabled;
11
+ const mqConfig = ctx.config.features.mutationQueue;
12
+ const batchSize = mqConfig.batchSize;
13
+ const batchDelayMs = mqConfig.batchDelayMs;
14
+ const maxRetries = mqConfig.maxRetries;
15
+ const retryBackoffMs = mqConfig.retryBackoffMs;
13
16
  const importLines = authEnabled
14
17
  ? `import { getAuth } from "./auth/store.${ext}";
15
18
  `
16
19
  : "";
17
- const additionalImports = `import { invalidateByTags } from "./cache.${ext}";
18
- ${ts ? `import type { MutationQueueItem } from "./swoff.d.ts";
19
- ` : ""}`;
20
- const authReplayBlock = authEnabled
20
+ const invalidateImport = tagInvalidation
21
+ ? `import { invalidateByTags } from "./cache.${ext}";
22
+ `
23
+ : "";
24
+ const additionalImports = `${invalidateImport}${ts
25
+ ? `import type { MutationQueueItem } from "./swoff.d.ts";
26
+ `
27
+ : ""}`;
28
+ const authReplayHeaders = authEnabled
21
29
  ? ` const auth = await getAuth();
22
30
  const authHeader${T("Record<string, string>")} = auth?.token ? { Authorization: \`Bearer \${auth.token}\` } : {};
23
31
  `
24
32
  : "";
25
- const authHeadersSpread = authEnabled
26
- ? ` ...authHeader,`
27
- : "";
33
+ const authHeaderSpread = authEnabled
34
+ ? ` ...authHeader,\n`
35
+ : " ";
28
36
  const code = `/**
29
37
  * Swoff Mutation Queue
30
38
  * Queue offline writes and sync when connection returns.
39
+ * Supports configurable batch size, rate limiting, and exponential backoff.
31
40
  *
32
41
  * Usage:
33
42
  * import { queueMutation, processMutationQueue, flushMutations, getPendingCount } from './swoff/mutation-queue.${ext}';
@@ -44,11 +53,24 @@ ${ts ? `import type { MutationQueueItem } from "./swoff.d.ts";
44
53
  * await flushMutations();
45
54
  *
46
55
  * // Auto-processes on online event
56
+ *
57
+ * Config:
58
+ * batchSize: ${batchSize} — mutations per progress event
59
+ * batchDelayMs: ${batchDelayMs} — delay between mutations (rate limiting)
60
+ * maxRetries: ${maxRetries} — max retries before dropping
61
+ * retryBackoffMs: ${retryBackoffMs} — exponential backoff base
47
62
  */
48
63
 
49
64
  ${importLines}${additionalImports}const DB_NAME = "swoff-queue";
50
65
  const STORE_NAME = "mutations";
51
- const MAX_RETRIES = 5;
66
+ const BATCH_SIZE = ${batchSize};
67
+ const BATCH_DELAY_MS = ${batchDelayMs};
68
+ const MAX_RETRIES = ${maxRetries};
69
+ const RETRY_BACKOFF_MS = ${retryBackoffMs};
70
+
71
+ function sleep(ms${T("number")})${R("Promise<void>")}{
72
+ return new Promise((r) => setTimeout(r, ms));
73
+ }
52
74
 
53
75
  function openQueueDB()${R("Promise<IDBDatabase>")}{
54
76
  return new Promise${PT("IDBDatabase")}((resolve, reject) => {
@@ -84,15 +106,21 @@ export async function queueMutation(mutation${T("Partial<MutationQueueItem>")})$
84
106
  bodyType = "buffer";
85
107
  }
86
108
 
109
+ // Strip auth headers before storing — tokens must never persist in IDB
110
+ const safeHeaders${T("Record<string, string>")} = { ...(mutation.headers || {}) };
111
+ delete safeHeaders["authorization"];
112
+ delete safeHeaders["Authorization"];
113
+
87
114
  store.add({
88
115
  id: crypto.randomUUID(),
89
116
  method: mutation.method,
90
117
  url: mutation.url,
91
118
  body,
92
119
  bodyType,
93
- headers: mutation.headers || {},
120
+ headers: safeHeaders,
94
121
  timestamp: Date.now(),
95
122
  retryCount: 0,
123
+ nextRetryAt: 0,
96
124
  tags: mutation.tags || [],
97
125
  });
98
126
 
@@ -104,13 +132,57 @@ export async function queueMutation(mutation${T("Partial<MutationQueueItem>")})$
104
132
  window.dispatchEvent(new CustomEvent("mutation-queue-changed"));
105
133
  }
106
134
 
107
- /** Process all queued mutations in order. Sends them to the server. Runs automatically on the online event. */
135
+ /** Replay a single queued mutation. Returns true on success, false on failure. */
136
+ async function replayMutation(item${T("MutationQueueItem")})${R("Promise<boolean>")}{
137
+ try {
138
+ ${authReplayHeaders} let replayBody${T("BodyInit | null")}${ts ? " = null" : ""};
139
+ let contentType${T("string | undefined")};
140
+ const bt = item.bodyType || "json";
141
+ if (bt === "formdata") {
142
+ replayBody = new FormData();
143
+ for (const [key, value] of (item.body || [])${AS("[string, FormDataEntryValue][]")}) {
144
+ replayBody.append(key, value);
145
+ }
146
+ } else if (bt === "blob") {
147
+ replayBody = item.body${AS("BodyInit | null")};
148
+ } else if (bt === "buffer") {
149
+ replayBody = item.body instanceof ArrayBuffer ? new Uint8Array(item.body)${AS("BodyInit")} : item.body${AS("BodyInit")};
150
+ } else {
151
+ replayBody = JSON.stringify(item.body);
152
+ contentType = "application/json";
153
+ }
154
+ const response = await fetch(item.url, {
155
+ method: item.method,
156
+ headers: {
157
+ ...(contentType ? { "Content-Type": contentType } : {}),
158
+ ...item.headers,
159
+ ${authHeaderSpread} },
160
+ body: replayBody,
161
+ });
162
+
163
+ if (!response.ok) throw new Error(\`HTTP \${response.status}\`);
164
+
165
+ if (item.tags && item.tags.length > 0) {
166
+ ${tagInvalidation ? "await invalidateByTags(item.tags);" : "// tagInvalidation disabled — skipping cache invalidation"}
167
+ }
168
+
169
+ await removeFromQueue(item.id);
170
+ return true;
171
+ } catch {
172
+ item.retryCount++;
173
+ item.nextRetryAt = Date.now() + RETRY_BACKOFF_MS * Math.pow(2, item.retryCount - 1);
174
+ await updateInQueue(item);
175
+ return false;
176
+ }
177
+ }
178
+
179
+ /** Process all queued mutations in order. Sends them to the server. Runs automatically when mutations are queued or on reconnection. Respects batchSize for progress reporting and batchDelayMs for rate limiting. */
108
180
  export async function processMutationQueue()${R("Promise<void>")}{
109
- if (!navigator.onLine || isSyncing) return;
181
+ if (isSyncing) return;
110
182
  isSyncing = true;
111
183
 
112
184
  try {
113
- ${authReplayBlock} const db = await openQueueDB();
185
+ const db = await openQueueDB();
114
186
  const tx = db.transaction(STORE_NAME, "readonly");
115
187
  const store = tx.objectStore(STORE_NAME);
116
188
  const index = store.index("by-timestamp");
@@ -120,8 +192,12 @@ ${authReplayBlock} const db = await openQueueDB();
120
192
  request.onerror = () => reject(request.error);
121
193
  });
122
194
 
195
+ if (queue.length === 0) return;
196
+
123
197
  let succeeded = 0;
124
198
  let failed = 0;
199
+ const total = queue.length;
200
+ let earliestRetry = Infinity;
125
201
 
126
202
  for (const item of queue) {
127
203
  if (item.retryCount >= MAX_RETRIES) {
@@ -130,52 +206,46 @@ ${authReplayBlock} const db = await openQueueDB();
130
206
  continue;
131
207
  }
132
208
 
133
- try {
134
- let replayBody${T("BodyInit | null")}${ts ? " = null" : ""};
135
- let contentType${T("string | undefined")};
136
- const bt = item.bodyType || "json";
137
- if (bt === "formdata") {
138
- replayBody = new FormData();
139
- for (const [key, value] of (item.body || [])${AS("[string, FormDataEntryValue][]")}) {
140
- replayBody.append(key, value);
141
- }
142
- } else if (bt === "blob") {
143
- replayBody = item.body${AS("BodyInit | null")};
144
- } else if (bt === "buffer") {
145
- replayBody = item.body instanceof ArrayBuffer ? new Uint8Array(item.body)${AS("BodyInit")} : item.body${AS("BodyInit")};
146
- } else {
147
- replayBody = JSON.stringify(item.body);
148
- contentType = "application/json";
149
- }
150
- const response = await fetch(item.url, {
151
- method: item.method,
152
- headers: {
153
- ...(contentType ? { "Content-Type": contentType } : {}),
154
- ${authHeadersSpread} ...item.headers,
155
- },
156
- body: replayBody,
157
- });
158
-
159
- if (!response.ok) throw new Error(\`HTTP \${response.status}\`);
160
-
161
- if (item.tags && item.tags.length > 0) {
162
- await invalidateByTags(item.tags);
163
- }
209
+ // Skip items whose backoff delay hasn't elapsed yet
210
+ if (item.nextRetryAt && Date.now() < item.nextRetryAt) {
211
+ if (item.nextRetryAt < earliestRetry) earliestRetry = item.nextRetryAt;
212
+ continue;
213
+ }
164
214
 
165
- await removeFromQueue(item.id);
215
+ const ok = await replayMutation(item);
216
+ if (ok) {
166
217
  succeeded++;
167
- } catch {
168
- item.retryCount++;
169
- await updateInQueue(item);
218
+ } else {
170
219
  failed++;
171
220
  }
221
+
222
+ // Rate limiting delay between mutations
223
+ if (BATCH_DELAY_MS > 0 && succeeded + failed < total) {
224
+ await sleep(BATCH_DELAY_MS);
225
+ }
226
+
227
+ // Emit progress after every BATCH_SIZE mutations
228
+ if ((succeeded + failed) % BATCH_SIZE === 0 || succeeded + failed === total) {
229
+ window.dispatchEvent(
230
+ new CustomEvent("mutation-sync-complete", {
231
+ detail: { succeeded, failed, total, current: succeeded + failed },
232
+ })
233
+ );
234
+ }
172
235
  }
173
236
 
174
237
  window.dispatchEvent(
175
238
  new CustomEvent("mutation-sync-complete", {
176
- detail: { succeeded, failed },
239
+ detail: { succeeded, failed, total },
177
240
  })
178
241
  );
242
+
243
+ // Schedule retry when the earliest backoff timer expires
244
+ if (earliestRetry < Infinity && earliestRetry > Date.now()) {
245
+ setTimeout(() => {
246
+ if (!isSyncing) processMutationQueue();
247
+ }, earliestRetry - Date.now());
248
+ }
179
249
  } finally {
180
250
  isSyncing = false;
181
251
  window.dispatchEvent(new CustomEvent("mutation-queue-changed"));
@@ -187,7 +257,6 @@ export async function flushMutations()${R("Promise<void>")}{
187
257
  await processMutationQueue();
188
258
  }
189
259
  `;
190
- // Helper functions appended outside the template string
191
260
  const helpers = `
192
261
  async function removeFromQueue(id${T("string")})${R("Promise<void>")}{
193
262
  const db = await openQueueDB();
@@ -219,6 +288,25 @@ export async function getPendingCount()${R("Promise<number>")}{
219
288
  request.onerror = () => reject((request${AS("IDBRequest")}).error);
220
289
  });
221
290
  }
291
+
292
+ /** Get the position of a mutation in the queue (0-based). Returns -1 if not found. */
293
+ export async function getQueuePosition(id${T("string")})${R("Promise<number>")}{
294
+ const items = await getQueueItems();
295
+ return items.findIndex((item) => item.id === id);
296
+ }
297
+
298
+ /** Get all pending queue items with their details. */
299
+ export async function getQueueItems()${R("Promise<MutationQueueItem[]>")}{
300
+ const db = await openQueueDB();
301
+ const tx = db.transaction(STORE_NAME, "readonly");
302
+ const store = tx.objectStore(STORE_NAME);
303
+ const index = store.index("by-timestamp");
304
+ return new Promise${PT("MutationQueueItem[]")}((resolve, reject) => {
305
+ const request = index.getAll();
306
+ request.onsuccess = () => resolve(request.result);
307
+ request.onerror = () => reject(request.error);
308
+ });
309
+ }
222
310
  `;
223
311
  writeFile(ctx, `mutation-queue.${ext}`, code + helpers);
224
312
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mutation-queue.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/mutation-queue.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mutation-queue.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/mutation-queue.ts"],"names":[],"mappings":"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"}