@swoff/cli 0.3.9 → 0.3.10
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/dist/__tests__/assemble-sw.test.js +8 -17
- package/dist/__tests__/assemble-sw.test.js.map +1 -1
- package/dist/__tests__/cli-integration.test.js +2 -2
- package/dist/__tests__/cli-integration.test.js.map +1 -1
- package/dist/__tests__/config-types.test.js +18 -8
- package/dist/__tests__/config-types.test.js.map +1 -1
- package/dist/__tests__/file-generators.test.js +3 -3
- package/dist/__tests__/file-generators.test.js.map +1 -1
- package/dist/__tests__/loader.test.js +5 -1
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/validator.test.js +6 -13
- package/dist/__tests__/validator.test.js.map +1 -1
- package/dist/index.js +39 -35
- package/dist/index.js.map +1 -1
- package/dist/lib/cli/help.js +13 -8
- package/dist/lib/cli/help.js.map +1 -1
- package/dist/lib/commands/add.js +38 -8
- package/dist/lib/commands/add.js.map +1 -1
- package/dist/lib/commands/assets.js +37 -0
- package/dist/lib/commands/assets.js.map +1 -0
- package/dist/lib/commands/clean.js +33 -17
- package/dist/lib/commands/clean.js.map +1 -1
- package/dist/lib/commands/generate-assets.js +44 -0
- package/dist/lib/commands/generate-assets.js.map +1 -0
- package/dist/lib/commands/generate-guide.js +22 -29
- package/dist/lib/commands/generate-guide.js.map +1 -1
- package/dist/lib/commands/generate.js +16 -9
- package/dist/lib/commands/generate.js.map +1 -1
- package/dist/lib/commands/info.js +36 -45
- package/dist/lib/commands/info.js.map +1 -1
- package/dist/lib/commands/init.js +25 -3
- package/dist/lib/commands/init.js.map +1 -1
- package/dist/lib/commands/validate.js +1 -1
- package/dist/lib/commands/validate.js.map +1 -1
- package/dist/lib/config/loader.js +25 -20
- package/dist/lib/config/loader.js.map +1 -1
- package/dist/lib/config/validator.js +20 -11
- package/dist/lib/config/validator.js.map +1 -1
- package/dist/lib/generators/asset-generator/dependency-resolver.js +76 -0
- package/dist/lib/generators/asset-generator/dependency-resolver.js.map +1 -0
- package/dist/lib/generators/asset-generator/generate.js +111 -0
- package/dist/lib/generators/asset-generator/generate.js.map +1 -0
- package/dist/lib/generators/asset-generator/guide.js +43 -0
- package/dist/lib/generators/asset-generator/guide.js.map +1 -0
- package/dist/lib/generators/asset-generator/html-patcher.js +65 -0
- package/dist/lib/generators/asset-generator/html-patcher.js.map +1 -0
- package/dist/lib/generators/asset-generator/ico-encoder.js +29 -0
- package/dist/lib/generators/asset-generator/ico-encoder.js.map +1 -0
- package/dist/lib/generators/asset-generator/manifest-patcher.js +23 -0
- package/dist/lib/generators/asset-generator/manifest-patcher.js.map +1 -0
- package/dist/lib/generators/asset-generator/maskable.js +15 -0
- package/dist/lib/generators/asset-generator/maskable.js.map +1 -0
- package/dist/lib/generators/asset-generator/rasterize.js +32 -0
- package/dist/lib/generators/asset-generator/rasterize.js.map +1 -0
- package/dist/lib/generators/asset-generator/sizes.js +25 -0
- package/dist/lib/generators/asset-generator/sizes.js.map +1 -0
- package/dist/lib/generators/file-generators/api-config.js +6 -0
- package/dist/lib/generators/file-generators/api-config.js.map +1 -0
- package/dist/lib/generators/file-generators/auth-state.js +2 -34
- package/dist/lib/generators/file-generators/auth-state.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-store.js +2 -250
- package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-user.js +3 -81
- package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
- package/dist/lib/generators/file-generators/background-sync.js +2 -57
- package/dist/lib/generators/file-generators/background-sync.js.map +1 -1
- package/dist/lib/generators/file-generators/cache.js +2 -30
- package/dist/lib/generators/file-generators/cache.js.map +1 -1
- package/dist/lib/generators/file-generators/client-injector.js +2 -130
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-state.js +6 -0
- package/dist/lib/generators/file-generators/fetch-state.js.map +1 -0
- package/dist/lib/generators/file-generators/fetch-wrapper.js +3 -295
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-framework-adapters.js +56 -0
- package/dist/lib/generators/file-generators/generate-framework-adapters.js.map +1 -0
- package/dist/lib/generators/file-generators/generate-hooks.js +4 -3
- package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
- package/dist/lib/generators/file-generators/gql-wrapper.js +3 -132
- package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/guide-generator.js +29 -1
- package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
- package/dist/lib/generators/file-generators/invalidation-tags.js +3 -258
- package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
- package/dist/lib/generators/file-generators/manifest.js +13 -3
- package/dist/lib/generators/file-generators/manifest.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-queue.js +3 -305
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-state.js +2 -117
- package/dist/lib/generators/file-generators/mutation-state.js.map +1 -1
- package/dist/lib/generators/file-generators/push.js +6 -138
- package/dist/lib/generators/file-generators/push.js.map +1 -1
- package/dist/lib/generators/file-generators/pwa-install.js +11 -84
- package/dist/lib/generators/file-generators/pwa-install.js.map +1 -1
- package/dist/lib/generators/file-generators/reset.js +6 -0
- package/dist/lib/generators/file-generators/reset.js.map +1 -0
- package/dist/lib/generators/file-generators/server-push.js +3 -134
- package/dist/lib/generators/file-generators/server-push.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-generator-build.js +4 -4
- package/dist/lib/generators/file-generators/sw-injector.js +9 -230
- package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/type-definitions.js +2 -111
- package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
- package/dist/lib/generators/sw-generator.js +5 -5
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js +25 -3
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/assemble-sw.js +13 -12
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/background-sync-handler.js +14 -7
- package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/config-header.js +1 -1
- package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
- package/dist/lib/generators/sw-sections/fetch-handler.js +165 -104
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/glob-matcher-code.js.map +1 -0
- package/dist/lib/generators/sw-sections/server-push-handler.js +12 -0
- package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/tag-management.js +6 -4
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +66 -35
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +89 -28
- package/dist/lib/shared/config-types.js.map +1 -1
- package/dist/runtime/api-config.js +18 -0
- package/dist/runtime/api-config.js.map +1 -0
- package/dist/runtime/auth-state.js +35 -0
- package/dist/runtime/auth-state.js.map +1 -0
- package/dist/runtime/auth-store.js +273 -0
- package/dist/runtime/auth-store.js.map +1 -0
- package/dist/runtime/auth-user.js +168 -0
- package/dist/runtime/auth-user.js.map +1 -0
- package/dist/runtime/background-sync.js +59 -0
- package/dist/runtime/background-sync.js.map +1 -0
- package/dist/runtime/cache.js +30 -0
- package/dist/runtime/cache.js.map +1 -0
- package/dist/runtime/client-injector.js +136 -0
- package/dist/runtime/client-injector.js.map +1 -0
- package/dist/runtime/fetch-state.js +42 -0
- package/dist/runtime/fetch-state.js.map +1 -0
- package/dist/runtime/fetch-wrapper.js +355 -0
- package/dist/runtime/fetch-wrapper.js.map +1 -0
- package/dist/runtime/gql-wrapper.js +135 -0
- package/dist/runtime/gql-wrapper.js.map +1 -0
- package/dist/runtime/invalidation-tags.js +260 -0
- package/dist/runtime/invalidation-tags.js.map +1 -0
- package/dist/runtime/mutation-queue.js +304 -0
- package/dist/runtime/mutation-queue.js.map +1 -0
- package/dist/runtime/mutation-state.js +117 -0
- package/dist/runtime/mutation-state.js.map +1 -0
- package/dist/runtime/push.js +138 -0
- package/dist/runtime/push.js.map +1 -0
- package/dist/runtime/pwa-index.js +11 -0
- package/dist/runtime/pwa-index.js.map +1 -0
- package/dist/runtime/pwa-injector.js +44 -0
- package/dist/runtime/pwa-injector.js.map +1 -0
- package/dist/runtime/pwa-install.js +83 -0
- package/dist/runtime/pwa-install.js.map +1 -0
- package/dist/runtime/pwa-prompt.js +54 -0
- package/dist/runtime/pwa-prompt.js.map +1 -0
- package/dist/runtime/reset.js +148 -0
- package/dist/runtime/reset.js.map +1 -0
- package/dist/runtime/server-push.js +148 -0
- package/dist/runtime/server-push.js.map +1 -0
- package/dist/runtime/sw-injector.js +236 -0
- package/dist/runtime/sw-injector.js.map +1 -0
- package/dist/runtime/type-definitions.js +138 -0
- package/dist/runtime/type-definitions.js.map +1 -0
- package/dist/runtime/utils.js +27 -0
- package/dist/runtime/utils.js.map +1 -0
- package/package.json +3 -2
- package/templates/react/useAuth.jsx +106 -0
- package/templates/react/useAuth.tsx +118 -0
- package/templates/{hooks → react}/useBackgroundSync.jsx +6 -4
- package/templates/{hooks → react}/useBackgroundSync.tsx +6 -4
- package/templates/{hooks → react}/useCacheInvalidation.jsx +2 -2
- package/templates/react/useCacheInvalidation.tsx +52 -0
- package/templates/react/useCachedFetch.jsx +140 -0
- package/templates/react/useCachedFetch.tsx +160 -0
- package/templates/react/useIsFetching.jsx +32 -0
- package/templates/react/useIsFetching.tsx +32 -0
- package/templates/react/useMutation.jsx +141 -0
- package/templates/react/useMutation.tsx +180 -0
- package/templates/react/useMutationQueue.jsx +69 -0
- package/templates/react/useMutationQueue.tsx +83 -0
- package/templates/react/useNetworkStatus.jsx +61 -0
- package/templates/react/useNetworkStatus.tsx +76 -0
- package/templates/react/usePrefetch.jsx +32 -0
- package/templates/react/usePrefetch.tsx +42 -0
- package/templates/{hooks → react}/usePushSubscription.jsx +4 -4
- package/templates/{hooks → react}/usePushSubscription.tsx +4 -4
- package/templates/{hooks → react}/useSWUpdate.jsx +3 -3
- package/templates/{hooks → react}/useSWUpdate.tsx +4 -4
- package/templates/react/useSwoffReset.jsx +32 -0
- package/templates/react/useSwoffReset.tsx +39 -0
- package/docs/API.md +0 -565
- package/docs/ARCHITECTURE.md +0 -374
- package/docs/CLI.md +0 -148
- package/docs/COMPARISON.md +0 -192
- package/docs/CONFIG.md +0 -277
- package/docs/ECOSYSTEM.md +0 -33
- package/templates/hooks/useAuth.jsx +0 -30
- package/templates/hooks/useAuth.tsx +0 -30
- package/templates/hooks/useCacheInvalidation.tsx +0 -19
- package/templates/hooks/useCachedFetch.jsx +0 -69
- package/templates/hooks/useCachedFetch.tsx +0 -81
- package/templates/hooks/useMutation.jsx +0 -76
- package/templates/hooks/useMutation.tsx +0 -109
- package/templates/hooks/useMutationQueue.jsx +0 -43
- package/templates/hooks/useMutationQueue.tsx +0 -50
- package/templates/hooks/useNetworkStatus.jsx +0 -19
- package/templates/hooks/useNetworkStatus.tsx +0 -19
- package/templates/hooks/usePrefetch.jsx +0 -10
- package/templates/hooks/usePrefetch.tsx +0 -25
- /package/templates/{hooks → react}/useMutationState.jsx +0 -0
- /package/templates/{hooks → react}/useMutationState.tsx +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gql-wrapper.js","sourceRoot":"","sources":["../../src/runtime/gql-wrapper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,UAAU,sBAAsB,CACpC,GAAmB,EACnB,QAAgB;IAEhB,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,GAAG,CAAC;IAExB,MAAM,gBAAgB,GAAG,EAAE;QACzB,CAAC,CAAC;;;;;;;;CAQL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO;;;;;;;8EAOqE,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;gDA4BjC,GAAG;;EAEjD,gBAAgB;;iCAEe,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,eAAe,CAAC;;;;;;gCAM1C,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,SAAS,CAAC;;;;;;;6BAOtC,CAAC,CAAC,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,iBAAiB,CAAC;;;;;;;;8BAQ3C,CAAC,CAAC,EAAE,EAAE,eAAe,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,UAAU,CAAC;;;;;;;;;;oCAUrC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC;SACrC,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC;WACb,CAAC,CAAC,EAAE,EAAE,YAAY,CAAC;GAC3B,CAAC,CAAC,EAAE,EAAE,uBAAuB,CAAC;;;;;;;0DAOyB,QAAQ;;;;;;;;;;;;;;;;;;8BAkBpC,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC;;;;gCAIT,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC;SACjC,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC;aACX,CAAC,CAAC,EAAE,EAAE,qCAAqC,CAAC;WAC9C,CAAC,CAAC,EAAE,EAAE,YAAY,CAAC;GAC3B,CAAC,CAAC,EAAE,EAAE,uBAAuB,CAAC;uBACV,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC;;;;iCAIA,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC;YAC/B,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC;aACd,CAAC,CAAC,EAAE,EAAE,qCAAqC,CAAC;WAC9C,CAAC,CAAC,EAAE,EAAE,YAAY,CAAC;GAC3B,CAAC,CAAC,EAAE,EAAE,uBAAuB,CAAC;uBACV,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC;;CAEhC,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { T, R } from "./utils.js";
|
|
2
|
+
function escapeRegex(str) {
|
|
3
|
+
return str.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
4
|
+
}
|
|
5
|
+
function compilePatternEntry(rawPattern, tagTemplates) {
|
|
6
|
+
const rawParts = rawPattern.split("/");
|
|
7
|
+
const parts = rawParts.filter((p) => p !== "");
|
|
8
|
+
const paramNames = [];
|
|
9
|
+
const regexParts = [];
|
|
10
|
+
for (let i = 0; i < parts.length; i++) {
|
|
11
|
+
const part = parts[i];
|
|
12
|
+
if (part === "**") {
|
|
13
|
+
regexParts.push("(?:\\/[^/]+)*");
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (regexParts.length > 0) {
|
|
17
|
+
regexParts.push("\\/");
|
|
18
|
+
}
|
|
19
|
+
if (part === "*") {
|
|
20
|
+
regexParts.push("[^/]+");
|
|
21
|
+
}
|
|
22
|
+
else if (part.startsWith(":")) {
|
|
23
|
+
paramNames.push(part.slice(1));
|
|
24
|
+
regexParts.push("([^/]+)");
|
|
25
|
+
}
|
|
26
|
+
else if (part.includes("{")) {
|
|
27
|
+
const close = part.indexOf("}");
|
|
28
|
+
if (close !== -1) {
|
|
29
|
+
const inner = part.slice(part.indexOf("{") + 1, close);
|
|
30
|
+
const alternatives = inner.split(",").map((s) => escapeRegex(s.trim()));
|
|
31
|
+
regexParts.push("(?:" + alternatives.join("|") + ")");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
regexParts.push(escapeRegex(part));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
regexParts.push(escapeRegex(part));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (regexParts.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
const hasLeadingSlash = rawPattern.startsWith("/");
|
|
44
|
+
const regex = (hasLeadingSlash ? "^\\/" : "^") + regexParts.join("") + "$";
|
|
45
|
+
// Validate template params match captured params
|
|
46
|
+
const templatePlaceholders = new Set();
|
|
47
|
+
for (const tmpl of tagTemplates) {
|
|
48
|
+
const matches = tmpl.match(/\{(\w+)\}/g);
|
|
49
|
+
if (matches) {
|
|
50
|
+
for (const m of matches) {
|
|
51
|
+
templatePlaceholders.add(m.slice(1, -1));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const ph of templatePlaceholders) {
|
|
56
|
+
if (!paramNames.includes(ph)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { regex, params: paramNames, templates: tagTemplates };
|
|
61
|
+
}
|
|
62
|
+
function generatePatternCode(patterns) {
|
|
63
|
+
const entries = [];
|
|
64
|
+
for (const [pattern, templates] of Object.entries(patterns)) {
|
|
65
|
+
const compiled = compilePatternEntry(pattern, templates);
|
|
66
|
+
if (compiled) {
|
|
67
|
+
entries.push(` { re: new RegExp("${compiled.regex.replace(/\\/g, "\\\\")}"), params: ${JSON.stringify(compiled.params)}, templates: ${JSON.stringify(compiled.templates)} }`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return "[\n" + entries.join(",\n") + "\n]";
|
|
71
|
+
}
|
|
72
|
+
function generateSingularizationCode(singularization) {
|
|
73
|
+
if (!singularization || Object.keys(singularization).length === 0)
|
|
74
|
+
return "null";
|
|
75
|
+
return JSON.stringify(singularization);
|
|
76
|
+
}
|
|
77
|
+
function generateCascadingCode(cascading) {
|
|
78
|
+
if (!cascading || Object.keys(cascading).length === 0)
|
|
79
|
+
return "null";
|
|
80
|
+
return JSON.stringify(cascading);
|
|
81
|
+
}
|
|
82
|
+
export function generateInvalidationTagsCode(ctx, prefixes, patterns, singularization, cascading) {
|
|
83
|
+
const { ext, ts } = ctx;
|
|
84
|
+
const patternCode = generatePatternCode(patterns);
|
|
85
|
+
const singularizationCode = generateSingularizationCode(singularization);
|
|
86
|
+
const cascadingCode = generateCascadingCode(cascading);
|
|
87
|
+
const prefixesCode = JSON.stringify(prefixes);
|
|
88
|
+
return `/**
|
|
89
|
+
* Swoff Invalidation Tags Helper
|
|
90
|
+
* Pattern-based tag generation from URLs with configurable glob patterns,
|
|
91
|
+
* prefix skipping, singularization, and cascading invalidation.
|
|
92
|
+
*
|
|
93
|
+
* Usage:
|
|
94
|
+
* import { generateTags, invalidateUrl } from './swoff/cache/tags.${ext}';
|
|
95
|
+
*
|
|
96
|
+
* // Generate tags from URL
|
|
97
|
+
* generateTags("/api/todos"); // ["todos"]
|
|
98
|
+
* generateTags("/api/todos/42"); // ["todos", "todo:42"]
|
|
99
|
+
*
|
|
100
|
+
* // Use with fetch wrapper
|
|
101
|
+
* const data = await fetchWithCache("/api/todos", {
|
|
102
|
+
* tags: generateTags("/api/todos"),
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Invalidate after mutation
|
|
106
|
+
* await invalidateUrl("/api/todos/42");
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
import { invalidateByTags } from "./index.${ext}";
|
|
110
|
+
${ts ? `
|
|
111
|
+
interface TagPattern {
|
|
112
|
+
re: RegExp;
|
|
113
|
+
params: string[];
|
|
114
|
+
templates: string[];
|
|
115
|
+
}
|
|
116
|
+
` : ""}
|
|
117
|
+
// Pattern entries compiled from swoff.config.json
|
|
118
|
+
const TAG_PATTERNS${T(ts, "TagPattern[]")} = ${patternCode};
|
|
119
|
+
|
|
120
|
+
// URL path prefixes to skip during tag generation
|
|
121
|
+
const SKIP_PREFIXES = ${prefixesCode};
|
|
122
|
+
|
|
123
|
+
// Custom singularization map (plural → singular)
|
|
124
|
+
const SINGULARIZATION${T(ts, "Record<string, string> | null")} = ${singularizationCode};
|
|
125
|
+
|
|
126
|
+
// Cascading invalidation map (tag → dependent tags)
|
|
127
|
+
const CASCADING${T(ts, "Record<string, string[]> | null")} = ${cascadingCode};
|
|
128
|
+
|
|
129
|
+
/** Generate cache invalidation tags from a URL path. Tries configured patterns first, falls back to segment-based generation. */
|
|
130
|
+
export function generateTags(url${T(ts, "string | URL")})${R(ts, "string[]")}{
|
|
131
|
+
const parsed = typeof url === "string" ? new URL(url, window.location.origin) : url;
|
|
132
|
+
const path = parsed.pathname;
|
|
133
|
+
|
|
134
|
+
// Try configured patterns first
|
|
135
|
+
for (const entry of TAG_PATTERNS) {
|
|
136
|
+
const match = path.match(entry.re);
|
|
137
|
+
if (match) {
|
|
138
|
+
const params${T(ts, "Record<string, string>")} = {};
|
|
139
|
+
for (let i = 0; i < entry.params.length; i++) {
|
|
140
|
+
params[entry.params[i]] = match[i + 1];
|
|
141
|
+
}
|
|
142
|
+
return entry.templates.map((tmpl) => {
|
|
143
|
+
return tmpl.replace(/\\{(\\w+)\\}/g, (_, key) => params[key] ?? "");
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback: segment-based generation
|
|
149
|
+
const segments = path.split("/").filter(Boolean);
|
|
150
|
+
if (segments.length === 0) return ["root"];
|
|
151
|
+
|
|
152
|
+
let startIdx = 0;
|
|
153
|
+
while (startIdx < segments.length && SKIP_PREFIXES.includes(segments[startIdx])) {
|
|
154
|
+
startIdx++;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const resourceSegments = segments.slice(startIdx);
|
|
158
|
+
if (resourceSegments.length === 0) return ["root"];
|
|
159
|
+
|
|
160
|
+
const tags = [];
|
|
161
|
+
tags.push(resourceSegments[0]);
|
|
162
|
+
|
|
163
|
+
if (resourceSegments.length >= 2 && !isNaN(Number(resourceSegments[1]))) {
|
|
164
|
+
const collection = resourceSegments[0];
|
|
165
|
+
const id = resourceSegments[1];
|
|
166
|
+
const singular = SINGULARIZATION && SINGULARIZATION[collection] !== undefined
|
|
167
|
+
? SINGULARIZATION[collection]
|
|
168
|
+
: collection.replace(/s$/, "");
|
|
169
|
+
tags.push(\`\${singular}:\${id}\`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (let i = 2; i < resourceSegments.length; i++) {
|
|
173
|
+
if (isNaN(Number(resourceSegments[i]))) {
|
|
174
|
+
tags.push(resourceSegments[i]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return tags;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Generate tags prefixed by HTTP method. e.g. POST /api/todos → ["post-todos"]. */
|
|
182
|
+
export function generateTagsFromMethod(method${T(ts, "string")}, url${T(ts, "string | URL")})${R(ts, "string[]")}{
|
|
183
|
+
const tags = generateTags(url);
|
|
184
|
+
if (method === "GET" || method === "HEAD") return tags;
|
|
185
|
+
return tags.map((tag) => \`\${method.toLowerCase()}-\${tag}\`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Invalidate cached responses related to a URL, including cascading dependencies. */
|
|
189
|
+
export async function invalidateUrl(url${T(ts, "string | URL")})${R(ts, "Promise<void>")}{
|
|
190
|
+
const tags = generateTags(url);
|
|
191
|
+
const allTags = CASCADING ? expandCascading(tags) : tags;
|
|
192
|
+
await invalidateByTags(allTags);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Invalidate cached responses tagged with method-prefixed tags, including cascading dependencies. */
|
|
196
|
+
export async function invalidateByMethod(method${T(ts, "string")}, url${T(ts, "string | URL")})${R(ts, "Promise<void>")}{
|
|
197
|
+
const tags = generateTagsFromMethod(method, url);
|
|
198
|
+
const allTags = CASCADING ? expandCascading(tags) : tags;
|
|
199
|
+
await invalidateByTags(allTags);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Expand tags with their cascading dependencies, deduplicated. */
|
|
203
|
+
export function expandCascading(tags${T(ts, "string[]")})${R(ts, "string[]")}{
|
|
204
|
+
if (!CASCADING) return [...tags];
|
|
205
|
+
const result = new Set(tags);
|
|
206
|
+
for (const tag of tags) {
|
|
207
|
+
const deps = CASCADING[tag];
|
|
208
|
+
if (deps) {
|
|
209
|
+
for (const dep of deps) {
|
|
210
|
+
result.add(dep);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return [...result];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Introspect: get all URLs cached under a given tag. */
|
|
218
|
+
export async function getUrlsForTag(tag${T(ts, "string")})${R(ts, "Promise<{ url: string; actualUrl: string }[]>")}{
|
|
219
|
+
const controller = navigator.serviceWorker?.controller;
|
|
220
|
+
if (!controller) return [];
|
|
221
|
+
return new Promise((resolve) => {
|
|
222
|
+
const channel = new MessageChannel();
|
|
223
|
+
channel.port1.onmessage = (event) => {
|
|
224
|
+
resolve(event.data.urls || []);
|
|
225
|
+
};
|
|
226
|
+
controller.postMessage(
|
|
227
|
+
{ type: "GET_URLS_FOR_TAG", tag },
|
|
228
|
+
[channel.port2],
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Introspect: get all tags associated with a given URL. */
|
|
234
|
+
export async function getTagsForUrl(url${T(ts, "string")})${R(ts, "Promise<string[]>")}{
|
|
235
|
+
const controller = navigator.serviceWorker?.controller;
|
|
236
|
+
if (!controller) return [];
|
|
237
|
+
return new Promise((resolve) => {
|
|
238
|
+
const channel = new MessageChannel();
|
|
239
|
+
channel.port1.onmessage = (event) => {
|
|
240
|
+
resolve(event.data.tags || []);
|
|
241
|
+
};
|
|
242
|
+
controller.postMessage(
|
|
243
|
+
{ type: "GET_TAGS_FOR_URL", url },
|
|
244
|
+
[channel.port2],
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Invalidate all cached responses whose URL matches the given glob pattern. */
|
|
250
|
+
export async function invalidateMatching(glob${T(ts, "string")})${R(ts, "Promise<void>")}{
|
|
251
|
+
const controller = navigator.serviceWorker?.controller;
|
|
252
|
+
if (!controller) return;
|
|
253
|
+
controller.postMessage({
|
|
254
|
+
type: "INVALIDATE_MATCHING",
|
|
255
|
+
glob,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=invalidation-tags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalidation-tags.js","sourceRoot":"","sources":["../../src/runtime/invalidation-tags.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { T, R, PT, AS } from "./utils.js";
|
|
2
|
+
export function generateMutationQueueCode(ctx, authEnabled, batchSize, batchDelayMs, maxRetries, retryBackoffMs) {
|
|
3
|
+
const { ext, ts } = ctx;
|
|
4
|
+
const importLines = authEnabled
|
|
5
|
+
? `import { getAuth } from "../auth/store.${ext}";
|
|
6
|
+
`
|
|
7
|
+
: "";
|
|
8
|
+
const additionalImports = `import { invalidateByTags } from "../cache/index.${ext}";
|
|
9
|
+
${ts
|
|
10
|
+
? `import type { MutationQueueItem } from "../swoff.d.ts";
|
|
11
|
+
`
|
|
12
|
+
: ""}`;
|
|
13
|
+
const authReplayHeaders = authEnabled
|
|
14
|
+
? ` const auth = await getAuth();
|
|
15
|
+
const authHeader${T(ts, "Record<string, string>")} = auth?.token ? { Authorization: \`Bearer \${auth.token}\` } : {};
|
|
16
|
+
`
|
|
17
|
+
: "";
|
|
18
|
+
const authHeaderSpread = authEnabled
|
|
19
|
+
? ` ...authHeader,\n`
|
|
20
|
+
: " ";
|
|
21
|
+
const code = `/**
|
|
22
|
+
* Swoff Mutation Queue
|
|
23
|
+
* Queue offline writes and sync when connection returns.
|
|
24
|
+
* Supports configurable batch size, rate limiting, and exponential backoff.
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* import { queueMutation, processMutationQueue, flushMutations, getPendingCount } from './swoff/offline/queue.${ext}';
|
|
28
|
+
*
|
|
29
|
+
* // Queue a mutation
|
|
30
|
+
* await queueMutation({
|
|
31
|
+
* method: "POST",
|
|
32
|
+
* url: "/api/todos",
|
|
33
|
+
* body: { title: "Grocery" },
|
|
34
|
+
* tags: ["todos"],
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* // Flush queued mutations (e.g., after login)
|
|
38
|
+
* await flushMutations();
|
|
39
|
+
*
|
|
40
|
+
* // Auto-processes on online event
|
|
41
|
+
*
|
|
42
|
+
* Config:
|
|
43
|
+
* batchSize: ${batchSize} — mutations per progress event
|
|
44
|
+
* batchDelayMs: ${batchDelayMs} — delay between mutations (rate limiting)
|
|
45
|
+
* maxRetries: ${maxRetries} — max retries before dropping
|
|
46
|
+
* retryBackoffMs: ${retryBackoffMs} — exponential backoff base
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
${importLines}${additionalImports}const DB_NAME = "swoff-queue";
|
|
50
|
+
const STORE_NAME = "mutations";
|
|
51
|
+
// Bump this when adding new indexes/stores for schema migration
|
|
52
|
+
const DB_VERSION = 1;
|
|
53
|
+
const BATCH_SIZE = ${batchSize};
|
|
54
|
+
const BATCH_DELAY_MS = ${batchDelayMs};
|
|
55
|
+
const MAX_RETRIES = ${maxRetries};
|
|
56
|
+
const RETRY_BACKOFF_MS = ${retryBackoffMs};
|
|
57
|
+
|
|
58
|
+
function sleep(ms${T(ts, "number")})${R(ts, "Promise<void>")}{
|
|
59
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function openQueueDB()${R(ts, "Promise<IDBDatabase>")}{
|
|
63
|
+
return new Promise${PT(ts, "IDBDatabase")}((resolve, reject) => {
|
|
64
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
65
|
+
request.onupgradeneeded = (e) => {
|
|
66
|
+
const db = (e.target${AS(ts, "IDBOpenDBRequest")}).result;
|
|
67
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
68
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
69
|
+
store.createIndex("by-timestamp", "timestamp");
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
request.onsuccess = (e) => resolve((e.target${AS(ts, "IDBOpenDBRequest")}).result);
|
|
73
|
+
request.onerror = (e) => reject((e.target${AS(ts, "IDBRequest")}).error);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let isSyncing = false;
|
|
78
|
+
|
|
79
|
+
/** Store a write operation in IndexedDB for later sync. Works offline — use it for POST/PUT/PATCH/DELETE when the user might be offline. */
|
|
80
|
+
export async function queueMutation(mutation${T(ts, "Partial<MutationQueueItem>")})${R(ts, "Promise<void>")}{
|
|
81
|
+
const db = await openQueueDB();
|
|
82
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
83
|
+
const store = tx.objectStore(STORE_NAME);
|
|
84
|
+
|
|
85
|
+
let body = mutation.body;
|
|
86
|
+
let bodyType = "json";
|
|
87
|
+
if (typeof body === "string") {
|
|
88
|
+
bodyType = "text";
|
|
89
|
+
} else if (body instanceof FormData) {
|
|
90
|
+
bodyType = "formdata";
|
|
91
|
+
body = [...body.entries()];
|
|
92
|
+
} else if (body instanceof Blob) {
|
|
93
|
+
bodyType = "blob";
|
|
94
|
+
} else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
|
|
95
|
+
bodyType = "buffer";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Strip auth headers before storing — tokens must never persist in IDB
|
|
99
|
+
const safeHeaders${T(ts, "Record<string, string>")} = { ...(mutation.headers || {}) };
|
|
100
|
+
delete safeHeaders["authorization"];
|
|
101
|
+
delete safeHeaders["Authorization"];
|
|
102
|
+
|
|
103
|
+
store.add({
|
|
104
|
+
id: crypto.randomUUID(),
|
|
105
|
+
method: mutation.method,
|
|
106
|
+
url: mutation.url,
|
|
107
|
+
body,
|
|
108
|
+
bodyType,
|
|
109
|
+
headers: safeHeaders,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
retryCount: 0,
|
|
112
|
+
nextRetryAt: 0,
|
|
113
|
+
tags: mutation.tags || [],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await new Promise<void>((resolve, reject) => {
|
|
117
|
+
tx.oncomplete = () => resolve();
|
|
118
|
+
tx.onerror = () => reject(tx.error);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
window.dispatchEvent(new CustomEvent("mutation-queue-changed"));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Replay a single queued mutation. Uses the provided db connection if available. Returns true on success, false on failure. */
|
|
125
|
+
async function replayMutation(item${T(ts, "MutationQueueItem")}, db${T(ts, "IDBDatabase | undefined")})${R(ts, "Promise<boolean>")}{
|
|
126
|
+
try {
|
|
127
|
+
${authReplayHeaders} let replayBody${T(ts, "BodyInit | null")}${ts ? " = null" : ""};
|
|
128
|
+
let contentType${T(ts, "string | undefined")};
|
|
129
|
+
const bt = item.bodyType || "json";
|
|
130
|
+
if (bt === "formdata") {
|
|
131
|
+
replayBody = new FormData();
|
|
132
|
+
for (const [key, value] of (item.body || [])${AS(ts, "[string, FormDataEntryValue][]")}) {
|
|
133
|
+
replayBody.append(key, value);
|
|
134
|
+
}
|
|
135
|
+
} else if (bt === "blob") {
|
|
136
|
+
replayBody = item.body${AS(ts, "BodyInit | null")};
|
|
137
|
+
} else if (bt === "buffer") {
|
|
138
|
+
replayBody = item.body instanceof ArrayBuffer ? new Uint8Array(item.body)${AS(ts, "BodyInit")} : item.body${AS(ts, "BodyInit")};
|
|
139
|
+
} else if (bt === "text") {
|
|
140
|
+
replayBody = item.body${AS(ts, "BodyInit | null")};
|
|
141
|
+
} else if (item.body != null) {
|
|
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
|
+
...item.headers,
|
|
150
|
+
${authHeaderSpread} },
|
|
151
|
+
...(replayBody != null ? { 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, db);
|
|
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, db);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 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. */
|
|
171
|
+
export async function processMutationQueue()${R(ts, "Promise<void>")}{
|
|
172
|
+
if (isSyncing) return;
|
|
173
|
+
isSyncing = true;
|
|
174
|
+
|
|
175
|
+
const db = await openQueueDB();
|
|
176
|
+
try {
|
|
177
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
178
|
+
const store = tx.objectStore(STORE_NAME);
|
|
179
|
+
const index = store.index("by-timestamp");
|
|
180
|
+
const queue${T(ts, "MutationQueueItem[]")} = await new Promise${PT(ts, "MutationQueueItem[]")}((resolve, reject) => {
|
|
181
|
+
const request = index.getAll();
|
|
182
|
+
request.onsuccess = () => resolve(request.result);
|
|
183
|
+
request.onerror = () => reject(request.error);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (queue.length === 0) return;
|
|
187
|
+
|
|
188
|
+
let succeeded = 0;
|
|
189
|
+
let failed = 0;
|
|
190
|
+
const total = queue.length;
|
|
191
|
+
let earliestRetry = Infinity;
|
|
192
|
+
|
|
193
|
+
for (const item of queue) {
|
|
194
|
+
if (item.retryCount >= MAX_RETRIES) {
|
|
195
|
+
await removeFromQueue(item.id, db);
|
|
196
|
+
failed++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Skip items whose backoff delay hasn't elapsed yet
|
|
201
|
+
if (item.nextRetryAt && Date.now() < item.nextRetryAt) {
|
|
202
|
+
if (item.nextRetryAt < earliestRetry) earliestRetry = item.nextRetryAt;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const ok = await replayMutation(item, db);
|
|
207
|
+
if (ok) {
|
|
208
|
+
succeeded++;
|
|
209
|
+
} else {
|
|
210
|
+
failed++;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Rate limiting delay between mutations
|
|
214
|
+
if (BATCH_DELAY_MS > 0 && succeeded + failed < total) {
|
|
215
|
+
await sleep(BATCH_DELAY_MS);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Emit progress after every BATCH_SIZE mutations
|
|
219
|
+
if ((succeeded + failed) % BATCH_SIZE === 0 || succeeded + failed === total) {
|
|
220
|
+
window.dispatchEvent(
|
|
221
|
+
new CustomEvent("mutation-sync-complete", {
|
|
222
|
+
detail: { succeeded, failed, total, current: succeeded + failed },
|
|
223
|
+
})
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
window.dispatchEvent(
|
|
229
|
+
new CustomEvent("mutation-sync-complete", {
|
|
230
|
+
detail: { succeeded, failed, total },
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Schedule retry when the earliest backoff timer expires
|
|
235
|
+
if (earliestRetry < Infinity && earliestRetry > Date.now()) {
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
if (!isSyncing) processMutationQueue();
|
|
238
|
+
}, earliestRetry - Date.now());
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
db.close();
|
|
242
|
+
isSyncing = false;
|
|
243
|
+
window.dispatchEvent(new CustomEvent("mutation-queue-changed"));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Immediately process all queued mutations. Call this after re-login to flush mutations that failed due to auth expiry. */
|
|
248
|
+
export async function flushMutations()${R(ts, "Promise<void>")}{
|
|
249
|
+
await processMutationQueue();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function removeFromQueue(id${T(ts, "string")}, db${T(ts, "IDBDatabase | undefined")})${R(ts, "Promise<void>")}{
|
|
253
|
+
if (!db) db = await openQueueDB();
|
|
254
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
255
|
+
tx.objectStore(STORE_NAME).delete(id);
|
|
256
|
+
await new Promise${PT(ts, "void")}((resolve, reject) => {
|
|
257
|
+
tx.oncomplete = () => resolve();
|
|
258
|
+
tx.onerror = () => reject(tx.error);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function updateInQueue(item${T(ts, "MutationQueueItem")}, db${T(ts, "IDBDatabase | undefined")})${R(ts, "Promise<void>")}{
|
|
263
|
+
if (!db) db = await openQueueDB();
|
|
264
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
265
|
+
tx.objectStore(STORE_NAME).put(item);
|
|
266
|
+
await new Promise${PT(ts, "void")}((resolve, reject) => {
|
|
267
|
+
tx.oncomplete = () => resolve();
|
|
268
|
+
tx.onerror = () => reject(tx.error);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Get the number of queued mutations waiting to be synced. Useful for showing a sync badge. */
|
|
273
|
+
export async function getPendingCount()${R(ts, "Promise<number>")}{
|
|
274
|
+
const db = await openQueueDB();
|
|
275
|
+
return new Promise${PT(ts, "number")}((resolve, reject) => {
|
|
276
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
277
|
+
const request = tx.objectStore(STORE_NAME).count();
|
|
278
|
+
request.onsuccess = () => resolve((request${AS(ts, "IDBRequest<number>")}).result);
|
|
279
|
+
request.onerror = () => reject((request${AS(ts, "IDBRequest")}).error);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Get the position of a mutation in the queue (0-based). Returns -1 if not found. */
|
|
284
|
+
export async function getQueuePosition(id${T(ts, "string")})${R(ts, "Promise<number>")}{
|
|
285
|
+
const items = await getQueueItems();
|
|
286
|
+
return items.findIndex((item) => item.id === id);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Get all pending queue items with their details. */
|
|
290
|
+
export async function getQueueItems()${R(ts, "Promise<MutationQueueItem[]>")}{
|
|
291
|
+
const db = await openQueueDB();
|
|
292
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
293
|
+
const store = tx.objectStore(STORE_NAME);
|
|
294
|
+
const index = store.index("by-timestamp");
|
|
295
|
+
return new Promise${PT(ts, "MutationQueueItem[]")}((resolve, reject) => {
|
|
296
|
+
const request = index.getAll();
|
|
297
|
+
request.onsuccess = () => resolve(request.result);
|
|
298
|
+
request.onerror = () => reject(request.error);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
302
|
+
return code;
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=mutation-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation-queue.js","sourceRoot":"","sources":["../../src/runtime/mutation-queue.ts"],"names":[],"mappings":"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"}
|