Swoff Swoff

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:

URLGenerated 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 data

The 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: useCachedFetch listens for cache-invalidated events and auto-refetches when matching tags are found. useCacheInvalidation provides invalidateByTag, invalidateByTags, and invalidateUrl for manual invalidation.

  • Vue: useCacheInvalidation provides 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

On this page