Invalidation Tags
URL-based tag generation, cross-tab synchronization, and framework adapters for cache invalidation.
Prerequisites: You have the API Integration pattern set up with cache tagging.
URL-Based Tags
The invalidation-tags.js helper parses REST API URLs and generates cache tags:
| URL | Generated Tags |
|---|---|
/api/todos | ["todos"] |
/api/todos/42 | ["todos", "todo:42"] |
/api/todos/42/comments | ["todos", "todo:42", "comments"] |
/api/v1/users/123/posts | ["users", "user:123", "posts"] |
/graphql | ["root"] |
Common API prefixes (api, v1, v2, v3, rest, graphql, gql) are skipped automatically.
Generate with CLI
npx @swoff/cli generate
# Generates: swoff/invalidation-tags.js (when features.tagInvalidation is true)Usage
import { generateTags, invalidateUrl } from './swoff/invalidation-tags.js';
const tags = generateTags("/api/todos");
// → ["todos"]
const data = await fetchWithCache("/api/todos", {
tags: generateTags("/api/todos"),
}).then(r => r.json());
await invalidateUrl("/api/todos/42");
// Invalidates: ["todos", "todo:42"]REST API Patterns
// GET → tagged read
const todos = await fetchWithCache("/api/todos", {
tags: generateTags("/api/todos"),
}).then(r => r.json());
// POST create → invalidate collection
await fetchWithCache("/api/todos", {
method: "POST",
body: JSON.stringify({ title: "New task" }),
});
await invalidateUrl("/api/todos");
// PUT update → invalidate resource
await fetchWithCache("/api/todos/42", {
method: "PUT",
body: JSON.stringify({ title: "Updated" }),
});
await invalidateUrl("/api/todos/42");
// DELETE → invalidate resource + collection
await fetchWithCache("/api/todos/42", {
method: "DELETE",
});
await invalidateUrl("/api/todos/42");GraphQL
function graphqlTags(query) {
const match = query.trim().match(/^(query|mutation)\s+(\w+)/);
return match ? [match[2]] : ["graphql"];
}
const data = await fetchWithCache("/graphql", {
method: "POST",
headers: { "X-SW-Cache-Strategy": "read" },
tags: graphqlTags(`query todos { todos { id title } }`),
body: JSON.stringify({ query: `query todos { todos { id title } }` }),
}).then(r => r.json());
// → tags: ["todos"]Method-Prefixed Tags
The CLI also generates generateTagsFromMethod and invalidateByMethod which add the HTTP method prefix to tags:
import { generateTagsFromMethod, invalidateByMethod } from './swoff/invalidation-tags.js';
const tags = generateTagsFromMethod("GET", "/api/todos");
// → ["GET:todos"]
await invalidateByMethod("POST", "/api/todos");
// Invalidates: ["POST:todos"]Use these when you want to distinguish cache entries by HTTP method (e.g., GET:todos vs POST:todos).
Cross-Tab Sync
When a user has two tabs open and a mutation invalidates the cache in one tab, the cached data in the other tab stays stale. Cross-tab sync bridges this gap.
The Problem
Tab A: cached /api/todos with tag "todos"
Tab B: cached /api/todos with tag "todos"
User edits a todo in Tab B → invalidateByTag("todos")
→ SW clears Tab B's cache → ✅ Tab B refetches fresh data
→ Tab A still has stale cache → ❌ Tab A shows old dataThe Solution
After clearing cache entries, the SW broadcasts TAG_INVALIDATED to every connected client via clients.matchAll(). Each client refetches stale data automatically.
Setup
import { fetchWithCache } from "./swoff/fetch-wrapper.js";
import { initCrossTabSync } from "./swoff/cache.js";
// One-time setup — call after SW registration
initCrossTabSync();
// Watch for cross-tab invalidations
window.addEventListener("cache-invalidated", async (e) => {
const { tags } = e.detail;
if (tags.includes("todos")) {
const fresh = await fetchWithCache("/api/todos", {
tags: ["todos"],
}).then((r) => r.json());
updateUI(fresh);
}
});No SW changes needed — the SW template's invalidateByTag already broadcasts the message.
Framework Adapters
Cross-tab invalidation is handled automatically by the generated hooks:
-
React:
useCachedFetchlistens forcache-invalidatedevents and auto-refetches when matching tags are found.useCacheInvalidationprovidesinvalidateByTag,invalidateByTags, andinvalidateUrlfor manual invalidation. -
Vue:
useCacheInvalidationprovides the same invalidation helpers.
The cache-invalidated event is dispatched both locally and broadcast to other tabs via the service worker — no additional code needed.
Error Handling
Tag registration fails
If the SW hasn't activated yet when a tab calls invalidateByTag, the message may not reach the SW:
async function safeInvalidateByTag(tag, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
await invalidateByTag(tag);
return;
} catch (error) {
if (i === retries - 1) throw error;
await new Promise((r) => setTimeout(r, 500));
}
}
}Missing initCrossTabSync
If initCrossTabSync was not called, the cache-invalidated event is never dispatched and cross-tab sync silently does nothing. Detect it:
if (typeof window.__swoffCrossTabInit === "undefined") {
console.warn("Cross-tab sync not initialized. Call initCrossTabSync() after SW registration.");
}Next Steps
- Set up offline mutation queuing: Mutation Queuing
- Learn about conflict resolution for concurrent edits: Conflict Resolution
Swoff