@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.
- package/README.md +53 -606
- package/dist/__tests__/assemble-sw.test.js +8 -38
- package/dist/__tests__/assemble-sw.test.js.map +1 -1
- package/dist/__tests__/cli-integration.test.js +119 -11
- package/dist/__tests__/cli-integration.test.js.map +1 -1
- package/dist/__tests__/config-types.test.js +38 -18
- package/dist/__tests__/config-types.test.js.map +1 -1
- package/dist/__tests__/file-generators.test.js +2 -2
- package/dist/__tests__/file-generators.test.js.map +1 -1
- package/dist/__tests__/glob-matcher.test.js +86 -0
- package/dist/__tests__/glob-matcher.test.js.map +1 -0
- package/dist/__tests__/invalidation-tags.test.js +207 -23
- package/dist/__tests__/invalidation-tags.test.js.map +1 -1
- package/dist/__tests__/loader.test.js +12 -12
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/validator.test.js +170 -74
- package/dist/__tests__/validator.test.js.map +1 -1
- package/dist/index.js +12 -11
- package/dist/index.js.map +1 -1
- package/dist/lib/cli/help.js +0 -5
- package/dist/lib/cli/help.js.map +1 -1
- package/dist/lib/commands/add.js +52 -33
- package/dist/lib/commands/add.js.map +1 -1
- package/dist/lib/commands/clean.js +1 -15
- package/dist/lib/commands/clean.js.map +1 -1
- package/dist/lib/commands/generate-guide.js +5 -5
- package/dist/lib/commands/generate-guide.js.map +1 -1
- package/dist/lib/commands/generate.js +7 -21
- package/dist/lib/commands/generate.js.map +1 -1
- package/dist/lib/commands/info.js +37 -12
- package/dist/lib/commands/info.js.map +1 -1
- package/dist/lib/commands/init.js +2 -10
- package/dist/lib/commands/init.js.map +1 -1
- package/dist/lib/commands/validate.js +3 -3
- package/dist/lib/commands/validate.js.map +1 -1
- package/dist/lib/config/loader.js +3 -6
- package/dist/lib/config/loader.js.map +1 -1
- package/dist/lib/config/validator.js +192 -42
- package/dist/lib/config/validator.js.map +1 -1
- package/dist/lib/generators/file-generators/cache.js +2 -10
- package/dist/lib/generators/file-generators/cache.js.map +1 -1
- package/dist/lib/generators/file-generators/client-injector.js +51 -12
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-wrapper.js +127 -58
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-hooks.js +6 -2
- package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
- package/dist/lib/generators/file-generators/gql-wrapper.js +139 -0
- package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -0
- package/dist/lib/generators/file-generators/guide-generator.js +6 -436
- package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
- package/dist/lib/generators/file-generators/invalidation-tags.js +206 -19
- package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
- package/dist/lib/generators/file-generators/manifest.js +2 -2
- package/dist/lib/generators/file-generators/manifest.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-queue.js +140 -52
- 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 +6 -7
- package/dist/lib/generators/file-generators/push.js.map +1 -1
- package/dist/lib/generators/file-generators/quick-readme.js +8 -75
- package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
- package/dist/lib/generators/file-generators/server-push.js +146 -0
- package/dist/lib/generators/file-generators/server-push.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-generator-build.js +8 -9
- package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-injector.js +2 -1
- package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/type-definitions.js +26 -1
- package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
- package/dist/lib/generators/sw-generator.js +8 -18
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/assemble-sw.js +19 -11
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/background-sync-handler.js +80 -24
- package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/config-header.js +2 -2
- package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
- package/dist/lib/generators/sw-sections/default-template.js +2 -1
- package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
- package/dist/lib/generators/sw-sections/fetch-handler.js +461 -121
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/message-handler.js +55 -15
- package/dist/lib/generators/sw-sections/message-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/sw-push.js +67 -0
- package/dist/lib/generators/sw-sections/sw-push.js.map +1 -0
- package/dist/lib/generators/sw-sections/tag-management.js +62 -21
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +12 -16
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +133 -45
- package/dist/lib/shared/config-types.js.map +1 -1
- package/dist/lib/shared/glob-matcher.js +77 -0
- package/dist/lib/shared/glob-matcher.js.map +1 -0
- package/dist/lib/utils/tty-status.js +21 -0
- package/dist/lib/utils/tty-status.js.map +1 -0
- package/docs/API.md +565 -0
- package/docs/ARCHITECTURE.md +374 -0
- package/docs/CLI.md +148 -0
- package/docs/COMPARISON.md +192 -0
- package/docs/CONFIG.md +277 -0
- package/docs/ECOSYSTEM.md +33 -0
- package/package.json +2 -1
- package/templates/hooks/useAuth.jsx +2 -2
- package/templates/hooks/useAuth.tsx +2 -2
- package/templates/hooks/useCachedFetch.jsx +21 -7
- package/templates/hooks/useCachedFetch.tsx +23 -11
- package/templates/hooks/useMutation.jsx +76 -0
- package/templates/hooks/useMutation.tsx +109 -0
- package/templates/hooks/useMutationQueue.jsx +20 -11
- package/templates/hooks/useMutationQueue.tsx +31 -15
- package/templates/hooks/useMutationState.jsx +13 -0
- package/templates/hooks/useMutationState.tsx +25 -0
- package/templates/hooks/usePrefetch.jsx +10 -0
- package/templates/hooks/usePrefetch.tsx +25 -0
- package/templates/hooks/usePushSubscription.jsx +3 -3
- package/templates/hooks/usePushSubscription.tsx +3 -3
|
@@ -1,15 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generates invalidation-tags.js
|
|
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
|
-
*
|
|
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
|
-
|
|
146
|
+
// Cascading invalidation map (tag → dependent 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
|
|
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 &&
|
|
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
|
|
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"].
|
|
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
|
|
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
|
-
|
|
211
|
+
const allTags = CASCADING ? expandCascading(tags) : tags;
|
|
212
|
+
await invalidateByTags(allTags);
|
|
84
213
|
}
|
|
85
214
|
|
|
86
|
-
/** Invalidate cached responses tagged with method-prefixed tags
|
|
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
|
-
|
|
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
|
|
1
|
+
{"version":3,"file":"invalidation-tags.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/invalidation-tags.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
-
|
|
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;
|
|
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
|
|
18
|
-
|
|
19
|
-
`
|
|
20
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
/**
|
|
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 (
|
|
181
|
+
if (isSyncing) return;
|
|
110
182
|
isSyncing = true;
|
|
111
183
|
|
|
112
184
|
try {
|
|
113
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
215
|
+
const ok = await replayMutation(item);
|
|
216
|
+
if (ok) {
|
|
166
217
|
succeeded++;
|
|
167
|
-
}
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"mutation-queue.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/mutation-queue.ts"],"names":[],"mappings":"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"}
|