@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
@@ -0,0 +1,192 @@
1
+ # Offline-First Infrastructure: Library Comparison
2
+
3
+ ## Libraries Compared
4
+
5
+ | Library | Category | Approach | Runtime size |
6
+ | ------------------- | ------------------------- | ---------------------------------- | ---------------------------------------- |
7
+ | **Swoff** | All-in-one generator | Config-driven code gen | 0 kB |
8
+ | **Workbox** | SW toolkit | Build-time + runtime modules | ~30 kB |
9
+ | **vite-plugin-pwa** | SW (Vite) | Vite plugin wrapping Workbox | ~30 kB |
10
+ | **TanStack Query** | Server state | Runtime JS | 3.8 kB gzip |
11
+ | **SWR** | Server state | Runtime JS | 3.3 kB gzip |
12
+ | **RTK Query** | Server state | Runtime JS + Redux | 2.9 kB + Redux |
13
+ | **Apollo Client** | GraphQL client | Runtime JS | ~32 kB gzip |
14
+ | **RxDB** | Offline-first DB | Runtime JS | ~40 kB gzip |
15
+ | **TanStack DB** | Client DB + offline-first | Runtime JS (differential dataflow) | ~6 kB gzip core (+ SQLite WASM optional) |
16
+
17
+ ## Feature Matrix
18
+
19
+ ### Caching & Strategies
20
+
21
+ | Feature | Swoff | Workbox | TanStack Query | Apollo | RxDB | TanStack DB |
22
+ | --------------------------------- | ------------------- | ------------------ | -------------- | ---------------- | -------------- | ----------------------- |
23
+ | SW code generation | ✅ Full source | 🟡 Partial runtime | ❌ | ❌ | ❌ | ❌ |
24
+ | Caching strategies | ✅ 6 | ✅ 5 | ❌ | ❌ | ❌ | 🟡 via Query |
25
+ | Stale-while-revalidate | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ via Query |
26
+ | Cache-first / Network-first | ✅ both | ✅ both | ❌ | ❌ | ❌ | ❌ |
27
+ | Reactive strategy (staleTime) | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ via Query |
28
+ | Configurable mode (all/explicit) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
29
+ | Cache key normalization | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
30
+ | Ignore query params in cache key | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
31
+ | Precaching at install time | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
32
+
33
+ ### Invalidation
34
+
35
+ | Feature | Swoff | Workbox | TanStack Query | Apollo | RxDB | TanStack DB |
36
+ | --------------------------------- | ------------------- | ------------------ | -------------- | ---------------- | -------------- | ----------------------- |
37
+ | Tag-based invalidation | ✅ URL/op-name | ❌ | ✅ query-key | 🟡 custom | ❌ | ❌ query-key only |
38
+ | Cascading tag dependencies | ✅ client-expanded | ❌ | ❌ | ❌ | ❌ | ❌ |
39
+ | Glob-pattern invalidation | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
40
+ | Tag introspection (urls→tags, tags→urls) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
41
+ | Server push invalidation (SSE/WS) | ✅ built-in | ❌ | ❌ | ✅ subscriptions | ✅ replication | 🟡 via sync engine |
42
+ | SW confirmation before event | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
43
+ | De-duplicated refresh queue | ✅ batch + Map | ❌ | ❌ | ❌ | ❌ | ❌ |
44
+
45
+ ### Offline & Background Sync
46
+
47
+ | Feature | Swoff | Workbox | TanStack Query | Apollo | RxDB | TanStack DB |
48
+ | --------------------------------- | ------------------- | ------------------ | -------------- | ---------------- | -------------- | ----------------------- |
49
+ | Offline write queue | ✅ IndexedDB | 🟡 basic | ❌ | ❌ | ✅ | ✅ optimistic + offline |
50
+ | Configurable retry+backoff | ✅ | 🟡 | ❌ | ❌ | ❌ | 🟡 via Query |
51
+ | Background sync (post-tab-close) | ✅ SW via sync API | ✅ plugin | ❌ | ❌ | ❌ | ✅ offline tx |
52
+ | Per-mutation online check | ✅ before each | ❌ | ❌ | ❌ | ❌ | ❌ |
53
+ | Manual flush (after re-login) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
54
+ | Mutation progress tracking | ✅ per-item + batch | ❌ | ❌ | ❌ | ❌ | 🟡 per-item |
55
+
56
+ ### Service Worker
57
+
58
+ | Feature | Swoff | Workbox | TanStack Query | Apollo | RxDB | TanStack DB |
59
+ | --------------------------------- | ------------------- | ------------------ | -------------- | ---------------- | -------------- | ----------------------- |
60
+ | Generated auditable SW code | ✅ full source | 🟡 loader module | ❌ | ❌ | ❌ | ❌ |
61
+ | 3-tier config resolution | ✅ per-request → pattern → global | 🟡 2-tier | ❌ | ❌ | ❌ | ❌ |
62
+ | SW update versioning | ✅ 3 modes | ✅ runtime | ❌ | ❌ | ❌ | ❌ |
63
+ | Navigation preload | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
64
+ | SPA fallback for navigation | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
65
+ | SW lifecycle coordination | ✅ client-injector | ✅ built-in | ❌ | ❌ | ❌ | ❌ |
66
+
67
+ ### Auth
68
+
69
+ | Feature | Swoff | Workbox | TanStack Query | Apollo | RxDB | TanStack DB |
70
+ | --------------------------------- | ------------------- | ------------------ | -------------- | ---------------- | -------------- | ----------------------- |
71
+ | Built-in auth header injection | ✅ 3 types | ❌ | ❌ | ❌ | ❌ | ❌ |
72
+ | Auth-aware offline queue | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
73
+ | Token refresh before expiry | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
74
+ | 401 detection → clear auth | ✅ SW + client | ❌ | ❌ | ❌ | ❌ | ❌ |
75
+ | Memory-only token storage | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
76
+ | Offline user data cache | ✅ IndexedDB | ❌ | ❌ | ❌ | ❌ | ✅ |
77
+
78
+ ### GraphQL
79
+
80
+ | Feature | Swoff | Workbox | TanStack Query | Apollo | RxDB | TanStack DB |
81
+ | --------------------------------- | ------------------- | ------------------ | -------------- | ---------------- | -------------- | ----------------------- |
82
+ | Body-hash GQL caching | ✅ SHA-256 | ❌ | ❌ | ✅ in-memory | ❌ | 🟡 normalized cols |
83
+ | GQL operation-name auto-tags | ✅ | ❌ | ❌ | 🟡 custom | ❌ | ❌ |
84
+ | GQL offline mutation queue | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
85
+
86
+ ### Real-time & PWA
87
+
88
+ | Feature | Swoff | Workbox | TanStack Query | Apollo | RxDB | TanStack DB |
89
+ | --------------------------------- | ------------------- | ------------------ | -------------- | ---------------- | -------------- | ----------------------- |
90
+ | SSE/WS connection management | ✅ built-in | ❌ | ❌ | 🟡 link | ✅ replication | 🟡 via sync engine |
91
+ | PWA install prompt | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
92
+ | Push notifications | ✅ + React hook | ❌ | ❌ | ❌ | ❌ | ❌ |
93
+ | Manifest generation | ✅ | 🟡 | ❌ | ❌ | ❌ | ❌ |
94
+
95
+ ### Cross-tab & State
96
+
97
+ | Feature | Swoff | Workbox | TanStack Query | Apollo | RxDB | TanStack DB |
98
+ | --------------------------------- | ------------------- | ------------------ | -------------- | ---------------- | -------------- | ----------------------- |
99
+ | Cross-tab invalidation sync | ✅ SW-broadcast | ❌ | 🟡 limited | ❌ | ❌ | 🟡 LocalStorage |
100
+ | Tag registry (IndexedDB) | ✅ persistent | ❌ | ❌ | ❌ | ❌ | ❌ |
101
+ | Cache persistence model | Cache API + IDB | Cache API | In-memory | In-memory + IDB | IDB | IDB + SQLite |
102
+ | State survives page refresh | ✅ Cache API + IDB | ✅ Cache API | ❌ | 🟡 persisted | ✅ | ✅ |
103
+
104
+ ### Developer Experience
105
+
106
+ | Feature | Swoff | Workbox | TanStack Query | Apollo | RxDB | TanStack DB |
107
+ | --------------------------------- | ------------------- | ------------------ | -------------- | ---------------- | -------------- | ----------------------- |
108
+ | Zero runtime dependencies | ✅ generated code | ❌ Workbox | ❌ 3.8 kB | ❌ 32 kB | ❌ 40 kB | ❌ + Query + DB |
109
+ | Config-driven setup | ✅ single file | ✅ workbox-config | ❌ code-only | ❌ code-only | ❌ code-only | ❌ code-only |
110
+ | Build-tool agnostic | ✅ any | ✅ any | ✅ any | ✅ any | ✅ any | ✅ any |
111
+ | React hooks | ✅ 11 | 🟡 minimal | ✅ extensive | ✅ | ✅ | ✅ useLiveQuery |
112
+ | TypeScript declarations generated | ✅ | ❌ | ✅ built-in | ✅ built-in | ✅ built-in | ✅ built-in |
113
+ | Auditable generated code | ✅ every file | 🟡 only SW | ❌ | ❌ | ❌ | ❌ |
114
+
115
+ ## Edge Case Handling
116
+
117
+ | Edge Case | Swoff | TanStack Query / SWR | Workbox |
118
+ | --------------------------------- | ---------------------------------------- | -------------------- | --------------------------- |
119
+ | **Concurrent invalidation + SWR refresh** | Tagless refresh never clobbers invalidation entry in queue | N/A (no SW) | No tag system |
120
+ | **SW update during offline write** | Cache API + Tag IDB survive SW restart; refresh queue recreated on next fetch | N/A | Cached assets survive; runtime cache cleared if configured |
121
+ | **Tab crash mid-mutation replay** | SW detects no clients → replays via Background Sync | N/A (no SW) | No mutation system |
122
+ | **Rapid repeated invalidations** | Debounced INVALIDATE_TAG coalescing (configurable) | N/A | No tag system |
123
+ | **Offline mid-batch mutation replay** | Per-mutation online check stops processing; remaining mutations stay queued | N/A | No mutation system |
124
+ | **Query param cache-busting** | Configurable ignore params + sorted normalization | N/A | No SW-level caching |
125
+ | **Network flicker on reconnect** | Batch delay + retry backoff prevent stampede | ❌ no SW batching | ❌ per-request only |
126
+ | **staleVersions memory limit** | Max 100 entries, 30 min TTL; lost entries just skip stale fallback | N/A | N/A |
127
+ | **Tag IDB deleted mid-refresh** | Refresh re-populates tag IDB from queue entry's stored tags | N/A | N/A |
128
+ | **Dual-replay client + SW race** | SW checks clients.open() → skips entirely if any client page is open | N/A | No mutation system |
129
+ | **Background sync after all tabs close** | SW wakes via sync event, processes queue, IndexedDB persists | N/A | N/A |
130
+ | **Fetch error during stale refetch** | Exponential backoff retry via setTimeout keeps SW alive | Retry via hook | ❌ |
131
+ | **No service worker support** | SW-all features degrade gracefully; mutation queue falls back to client-only IDB | N/A (normal) | N/A (no SW = nothing) |
132
+
133
+ ### How each library handles persistence model failure
134
+
135
+ | Failure Scenario | Swoff | TanStack Query | RxDB | TanStack DB |
136
+ | --------------------------------- | ---------------------------------------- | -------------------- | --------------------- | --------------------- |
137
+ | Memory cleared (tab close/refresh) | ✅ Tag IDB + Cache API survive | ❌ Full loss | ✅ IDB survives | ✅ IDB survives |
138
+ | SW terminated (idle) | ✅ IDB + Cache API survive; Maps rebuilt on next wake | N/A | N/A | N/A |
139
+ | SW killed during fetch handler | ✅ Cache API atomic; partial writes discarded | N/A | N/A | N/A |
140
+ | IndexedDB quota exceeded | ✅ Cache API still functional; only tag introspection lost | N/A | ❌ Full loss | ❌ Full loss |
141
+ | Browser storage cleared | ✅ All caches lost, SW stays installed; next generation re-caches | ❌ | ❌ | ❌ |
142
+
143
+ ### How Swoff handles race conditions
144
+
145
+ | Race Condition | Mitigation |
146
+ | --------------------------------- | ---------------------------------------- |
147
+ | SWR queues refresh without tags → invalidation queues with tags | `queueRefresh` guard: tagless call doesn't override if entry with tags exists |
148
+ | Refresh completes after invalidation deletes tag IDB | `_processRefreshQueue` re-populates tags from queue entry |
149
+ | Client triggers `invalidateByTag` before SW caches response | SW processes `INVALIDATE_TAG` at message time; if cache entry doesn't exist yet, it's a no-op for that URL |
150
+ | Multiple tabs call `invalidateByTag` simultaneously | SW debounce (configurable) + Map dedup on tag; single IDB scan |
151
+ | SW and client both try to replay mutation queue | SW skips entirely if any client page is open |
152
+ | Refresh fails → staleVersions entry → next SWR serves stale | staleVersions TTL 30 min, max 100 entries; serves old cache as fallback until retry succeeds |
153
+
154
+ ## Key Differentiators
155
+
156
+ **Where Swoff leads:**
157
+
158
+ - **Offline-first integration** — combines SW caching, data fetching, auth, mutation queue, GraphQL, PWA, push notifications, cross-tab sync, and real-time invalidation in a single config-driven system. Others require 4-6 separate tools.
159
+ - **Zero runtime dependencies** — generated code uses only browser APIs. No bundle impact, no version mismatches.
160
+ - **Auth across the stack** — token injection, 401 handling, refresh, offline user caching, and state detection built into fetch wrapper, mutation queue, and SW.
161
+ - **Generated auditable code** — every line visible in `swoff/`. Read, edit, and commit it.
162
+ - **Config-driven resolution** — 3-tier priority for strategy (per-request → route pattern → global default).
163
+ - **SW confirmation-driven events** — `cache-invalidated` fires only after SW confirms via `TAG_INVALIDATED`, not before SW clears cache. Eliminates the "event before action" race.
164
+ - **Tag registry repopulation on refresh** — after invalidation deletes tag→URL mappings from IDB, the background refresh re-populates them. Subsequent invalidations still find the URL.
165
+ - **SW-broadcast cross-tab invalidation** — `TAG_INVALIDATED` is posted to every connected client via `self.clients.matchAll()`. No BroadcastChannel needed.
166
+
167
+ **Where TanStack DB leads (and Swoff still needs to catch up):**
168
+
169
+ - **Optimistic updates** — instant local writes with automatic rollback on failure.
170
+ - **Normalized collections** — query across related data with joins, filters, and aggregates.
171
+ - **Differential dataflow** — sub-millisecond incremental query re-computation even with 100k+ items.
172
+ - **SSR support** — works with server rendering (TanStack DB is actively developing this).
173
+
174
+ **Where Swoff still leads vs TanStack DB:**
175
+
176
+ - **SW-level caching** — Swoff intercepts at the `fetch` event, caching even third-party and plain `fetch()` calls. TanStack DB operates at the application layer only.
177
+ - **Auth across the stack** — token injection, 401 handling, refresh in SW + client.
178
+ - **Zero runtime deps** — generated code vs TanStack DB's ~6 kB core + Query + optional SQLite WASM.
179
+ - **Config-driven setup** — one config file vs code-only setup.
180
+ - **PWA + push notifications** — built into the generated output.
181
+
182
+ **Where Swoff still needs to catch up:**
183
+
184
+ - **Framework hooks** — React only. Vue, Svelte, Solid adapters planned.
185
+ - **Infinite queries / pagination** — not yet built.
186
+ - **Optimistic updates** — not yet built.
187
+ - **Normalized GraphQL cache** — body-hash caching is simple but cannot merge overlapping query results like Apollo/Relay.
188
+ - **Devtools** — no browser extension yet.
189
+
190
+ ## Architecture fit
191
+
192
+ Swoff operates at the **browser infrastructure layer** (Service Worker + `fetch` event), not the application layer. This means it works with any backend (PHP, Laravel, Django, Rails, Go, Java), any frontend (React, Vue, Svelte, HTMX, vanilla), and any rendering strategy (SSR, SSG, SPA, islands, HTML-over-wire). See [ECOSYSTEM.md](./ECOSYSTEM.md).
package/docs/CONFIG.md ADDED
@@ -0,0 +1,277 @@
1
+ # Configuration Reference
2
+
3
+ Full schema for `swoff.config.json` — every field, its type, default, and description.
4
+
5
+ ## Quick reference
6
+
7
+ ```json
8
+ {
9
+ "$schema": "https://swoff.netlify.app/schema/v1.json",
10
+ "enabled": true,
11
+ "framework": "react",
12
+ "build": {
13
+ "outputDir": "dist",
14
+ "swFilename": "sw"
15
+ },
16
+ "features": {
17
+ "pwa": {
18
+ "enabled": true,
19
+ "preventDefaultInstall": false
20
+ },
21
+ "serviceWorker": {
22
+ "version": "package",
23
+ "minSupportedVersion": "1.0.0",
24
+ "autoUpdate": true,
25
+ "autoActivate": false,
26
+ "strategy": {
27
+ "default": "cache-first",
28
+ "patterns": {
29
+ "/api/*": { "strategy": "reactive", "staleTime": 30, "refetchOnFocus": true },
30
+ "/static/*": "cache-first"
31
+ },
32
+ "reactive": {
33
+ "defaults": {
34
+ "staleTime": 0,
35
+ "refetchInterval": 0,
36
+ "refetchOnReconnect": false,
37
+ "refetchOnFocus": false
38
+ }
39
+ },
40
+ "mode": "all",
41
+ "clearRuntimeOnUpdate": false,
42
+ "normalizeKey": false,
43
+ "ignoreQueryParams": []
44
+ },
45
+ "navigation": {
46
+ "mode": "spa",
47
+ "preload": true,
48
+ "fallback": "/index.html"
49
+ }
50
+ },
51
+ "refetchQueue": {
52
+ "batchSize": 5,
53
+ "batchDelayMs": 1000,
54
+ "maxRetries": 3,
55
+ "retryDelayMs": 1000
56
+ },
57
+ "mutationQueue": {
58
+ "enabled": false,
59
+ "batchSize": 1,
60
+ "batchDelayMs": 0,
61
+ "maxRetries": 5,
62
+ "retryBackoffMs": 1000
63
+ },
64
+ "backgroundSync": false,
65
+ "auth": {
66
+ "enabled": false,
67
+ "type": "bearer",
68
+ "refreshPath": "/api/refresh",
69
+ "userEndpoint": "/api/me"
70
+ },
71
+ "graphql": {
72
+ "enabled": false,
73
+ "endpoint": "/graphql"
74
+ },
75
+ "crossTabSync": true,
76
+ "tagInvalidation": {
77
+ "enabled": true
78
+ },
79
+ "pushNotifications": {
80
+ "enabled": false,
81
+ "vapidPublicKey": ""
82
+ },
83
+ "serverPush": {
84
+ "enabled": false,
85
+ "type": "sse",
86
+ "endpoint": "/api/events",
87
+ "reconnectDelayMs": 5000
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Top-level fields
96
+
97
+ | Field | Type | Default | Description |
98
+ |-------|------|---------|-------------|
99
+ | `$schema` | `string` | — | JSON Schema URL (for IDE autocomplete) |
100
+ | `enabled` | `boolean` | `true` | Master switch — disables all Swoff features |
101
+ | `framework` | `"react"` \| `"vue"` \| `"svelte"` \| `"vanilla"` | auto-detected | Your UI framework. Controls whether React hooks are generated. |
102
+ | `build.outputDir` | `string` | `"dist"` | Build tool output directory |
103
+ | `build.swFilename` | `string` | `"sw"` | Service worker filename prefix (e.g. `sw-v1.2.3.js`) |
104
+
105
+ ---
106
+
107
+ ## `features.serviceWorker`
108
+
109
+ | Field | Type | Default | Description |
110
+ |-------|------|---------|-------------|
111
+ | `version` | `"package"` \| `"hash"` \| `string` \| `false` | `"package"` | SW version mode. `"package"`: reads from `package.json`. `"hash"`: SHA-256 hash of generated SW content (deterministic, fixed SW URL). Semver string (e.g. `"1.2.3"`): explicit version. `false`: no versioning (simple mode, hash-based cache name). |
112
+ | `minSupportedVersion` | `string` | `"0.0.0"` | Minimum supported SW version — clients below this are force-updated on page load |
113
+ | `autoUpdate` | `boolean` | `true` | Automatically register new service worker versions when detected. When false, dispatches `sw-update-available` event for manual registration via `handleUpdateApproved()`. |
114
+ | `autoActivate` | `boolean` | `false` | Automatically activate newly registered service workers (`skipWaiting`). Only applies when a new version is registered — use with `autoUpdate` for fully silent updates, or wait for user consent then call `handleUpdateApproved()`. |
115
+
116
+ ### `features.serviceWorker.strategy`
117
+
118
+ | Field | Type | Default | Description |
119
+ |-------|------|---------|-------------|
120
+ | `default` | `string` | `"cache-first"` | Default caching strategy (lowest priority in 3-tier resolution). One of: `cache-first`, `network-first`, `stale-while-revalidate`, `cache-only`, `network-only`, `reactive` |
121
+ | `patterns` | `object` | `{}` | Per-route strategy overrides. Keys are URL patterns (e.g. `/api/*`). Values can be a strategy name string or a `StrategyEntry` object |
122
+ | `reactive.defaults.staleTime` | `number` | `0` | Global stale time in seconds for reactive strategy. Data is considered fresh for this long; after expiry, SW serves cached but triggers a background refresh. 0 = always stale. |
123
+ | `reactive.defaults.refetchInterval` | `number` | `0` | Global auto-refetch interval in seconds for reactive strategy. SW periodically re-fetches matching routes in the background. 0 disables. |
124
+ | `reactive.defaults.refetchOnReconnect` | `boolean` | `false` | Global default — refetch reactive entries when the browser comes back online |
125
+ | `reactive.defaults.refetchOnFocus` | `boolean` | `false` | Global default — refetch reactive entries when the tab gains focus |
126
+ | `mode` | `"all"` \| `"explicit-only"` | `"all"` | When to apply caching strategies. `"all"`: every GET/HEAD request; `"explicit-only"`: only requests with `X-SW-Cache-Strategy` header |
127
+ | `clearRuntimeOnUpdate` | `boolean` | `false` | Clear runtime cache when a new SW version activates |
128
+ | `normalizeKey` | `boolean` | `false` | When `true`, query params are sorted alphabetically in cache keys so `?b=1&a=2` and `?a=2&b=1` resolve to the same entry. |
129
+ | `ignoreQueryParams` | `string[]` | `[]` | Query params to strip from cache keys (e.g. `["_t", "utm_source"]`). Prevents cache-busting params from creating duplicate cache entries. |
130
+
131
+ ### `features.serviceWorker.navigation`
132
+
133
+ | Field | Type | Default | Description |
134
+ |-------|------|---------|-------------|
135
+ | `mode` | `"spa"` \| `"default"` | `"spa"` | SPA mode sends unmatched navigation requests to `fallback` |
136
+ | `preload` | `boolean` | `true` | Enable Navigation Preload API — reduces SW startup latency |
137
+ | `fallback` | `string` | `"/index.html"` | Fallback HTML for SPA navigation requests |
138
+
139
+ ### `StrategyEntry` object
140
+
141
+ When a strategy value is an object instead of a string:
142
+
143
+ ```json
144
+ "/api/*": {
145
+ "strategy": "reactive",
146
+ "staleTime": 30,
147
+ "refetchOnFocus": true
148
+ }
149
+ ```
150
+
151
+ | Field | Type | Default | Description |
152
+ |-------|------|---------|-------------|
153
+ | `strategy` | `string` | (required) | Caching strategy name. One of: `cache-first`, `network-first`, `stale-while-revalidate`, `cache-only`, `network-only`, `reactive` |
154
+ | `staleTime` | `number` | — | Stale time in seconds. **Reactive-only** — only valid when `strategy` is `"reactive"`. Data is considered fresh for this long; after expiry, SW serves cached but triggers a background refresh. 0 = always stale. |
155
+ | `refetchInterval` | `number` | — | Auto-refetch interval in seconds. **Reactive-only**. The SW periodically re-fetches this route in the background. 0 disables. |
156
+ | `refetchOnReconnect` | `boolean` | `false` | Refetch when the browser comes back online. **Reactive-only**. |
157
+ | `refetchOnFocus` | `boolean` | `false` | Refetch when the browser tab gains focus. **Reactive-only**. |
158
+
159
+ ---
160
+
161
+ ## `features.refetchQueue`
162
+
163
+ | Field | Type | Default | Description |
164
+ |-------|------|---------|-------------|
165
+ | `batchSize` | `number` | `5` | Max stale cache entries to refetch per batch |
166
+ | `batchDelayMs` | `number` | `1000` | Delay in ms between batch cycles (rate limiting) |
167
+ | `maxRetries` | `number` | `3` | Max retries for background refetches (exponential backoff) |
168
+ | `retryDelayMs` | `number` | `1000` | Base delay in ms for retry backoff (delay × 2^retryCount) |
169
+
170
+ ---
171
+
172
+ ### Resolution tiers
173
+
174
+ `strategy` and `staleTime` resolve in 3 tiers (highest to lowest priority):
175
+
176
+ 1. **Per-request** — passed as options to `fetchWithCache({ strategy })` or `useCachedFetch()`, or via `X-SW-Strategy` / `X-SW-Stale-Time` headers
177
+ 2. **Route pattern** — configured in `features.serviceWorker.strategy.patterns`
178
+ 3. **Global default** — configured at `features.serviceWorker.strategy.reactive.defaults.*` (for `staleTime`) or `features.serviceWorker.strategy.default` (for `strategy`)
179
+
180
+ `refetchInterval`, `refetchOnReconnect`, and `refetchOnFocus` are SW-initiated (interval timers, online/focus event handlers) and resolve in 2 tiers only:
181
+
182
+ 1. **Route pattern** — configured in the pattern entry
183
+ 2. **Global default** — configured at `features.serviceWorker.strategy.reactive.defaults.*`
184
+
185
+ ---
186
+
187
+ ## `features.mutationQueue`
188
+
189
+ Object-only feature (boolean shorthand not supported).
190
+
191
+ | Field | Type | Default | Description |
192
+ |-------|------|---------|-------------|
193
+ | `enabled` | `boolean` | `false` | Enable offline write queue |
194
+ | `batchSize` | `number` | `1` | Mutations per progress event |
195
+ | `batchDelayMs` | `number` | `0` | Delay between mutations (rate limiting) |
196
+ | `maxRetries` | `number` | `5` | Max retries before dropping a mutation |
197
+ | `retryBackoffMs` | `number` | `1000` | Exponential backoff base (nextRetry = backoff × 2^retryCount) |
198
+
199
+ ---
200
+
201
+ ## `features.auth`
202
+
203
+ | Field | Type | Default | Description |
204
+ |-------|------|---------|-------------|
205
+ | `enabled` | `boolean` | `false` | Enable auth module |
206
+ | `type` | `"bearer"` \| `"cookie"` \| `"custom"` | `"bearer"` | Auth strategy |
207
+ | `refreshPath` | `string` | `"/api/refresh"` | Token refresh endpoint |
208
+ | `userEndpoint` | `string` | `"/api/me"` | Current user fetch endpoint |
209
+
210
+ ---
211
+
212
+ ## `features.graphql`
213
+
214
+ Object-only feature (boolean shorthand not supported).
215
+
216
+ | Field | Type | Default | Description |
217
+ |-------|------|---------|-------------|
218
+ | `enabled` | `boolean` | `false` | Generate GraphQL wrapper (`queryGql` / `mutateGql`) |
219
+ | `endpoint` | `string` | `"/graphql"` | GraphQL endpoint URL |
220
+
221
+ ---
222
+
223
+ ## `features.pushNotifications`
224
+
225
+ | Field | Type | Default | Description |
226
+ |-------|------|---------|-------------|
227
+ | `enabled` | `boolean` | `false` | Generate push notification subscription management |
228
+ | `vapidPublicKey` | `string` | `""` | VAPID public key. Baked into the generated push handler at build time — not needed at runtime. |
229
+
230
+ ---
231
+
232
+ ## `features.serverPush`
233
+
234
+ | Field | Type | Default | Description |
235
+ |-------|------|---------|-------------|
236
+ | `enabled` | `boolean` | `false` | Enable real-time cache invalidation via SSE or WebSocket |
237
+ | `type` | `"sse"` \| `"websocket"` | `"sse"` | Transport protocol |
238
+ | `endpoint` | `string` | `"/api/events"` | Push endpoint URL |
239
+ | `reconnectDelayMs` | `number` | `5000` | Initial reconnect delay on connection loss (exponential backoff, capped at 30s) |
240
+
241
+ ---
242
+
243
+ ## `features.tagInvalidation`
244
+
245
+ Object-only feature (boolean shorthand not supported).
246
+
247
+ | Field | Type | Default | Description |
248
+ |-------|------|---------|-------------|
249
+ | `enabled` | `boolean` | `true` | Enable tag-based cache invalidation |
250
+ | `debounceMs` | `number` | `0` | Debounce window (ms) for coalescing rapid invalidations. When > 0, repeated `INVALIDATE_TAG` messages within the window are batched and processed once, reducing redundant cache scans. |
251
+ | `prefixes` | `string[]` | `["api","v1","v2","v3","rest","graphql","gql"]` | URL path prefixes to skip during tag generation |
252
+ | `patterns` | `object` | `{}` | Custom glob patterns for tag generation. Keys are URL patterns (`/api/:id`), values are tag template arrays (`["{id}"]`) |
253
+ | `singularization` | `object` | `{}` | Custom plural→singular mapping (e.g. `{"categories": "category"}`). Default: strips trailing `s`. |
254
+ | `cascading` | `object` | `{}` | Cascading tag dependencies. `{"todos": ["categories"]}` — invalidating `todos` also invalidates `categories` |
255
+
256
+ ---
257
+
258
+ ## Boolean features
259
+
260
+ | Feature | Config path | Default |
261
+ |---------|-------------|---------|
262
+ | Background Sync | `features.backgroundSync` | `false` |
263
+ | Cross-tab Sync | `features.crossTabSync` | `true` |
264
+
265
+ ---
266
+
267
+ ## Feature dependencies
268
+
269
+ Some features work best together:
270
+
271
+ | Feature | Recommended combo | Why |
272
+ |---------|------------------|-----|
273
+ | `mutationQueue` | + `backgroundSync` | Background Sync processes mutations even after tab close |
274
+ | `tagInvalidation` | + `crossTabSync` | Invalidation events broadcast to all open tabs |
275
+ | `serverPush` | + `tagInvalidation` | Server push triggers `invalidateByTag()` — requires tag invalidation to function |
276
+ | `auth` + `mutationQueue` | — | `flushMutations()` after re-login replays mutations that failed with 401 |
277
+ | `graphql` | + `mutationQueue` + `tagInvalidation` | Offline GQL mutations queue in IndexedDB; mutations auto-invalidate operation-name tags |
@@ -0,0 +1,33 @@
1
+ # Ecosystem Compatibility
2
+
3
+ Swoff operates at the **`fetch` event** + **Service Worker** layer — below the application and data-fetching layers. This makes it compatible with any web stack regardless of backend language, frontend framework, or rendering strategy.
4
+
5
+ ```
6
+ Application Layer (React, Vue, Svelte, HTMX, vanilla)
7
+ Data Fetching Layer (TanStack Query, SWR, Apollo, fetch())
8
+ SWOFF — SW + Client (fetch interception, cache, offline)
9
+ Browser APIs (Cache Storage, IndexedDB, fetch, push)
10
+ ```
11
+
12
+ The only framework-specific surface is **view adapters** (React hooks, future Vue/Svelte). These are optional thin wrappers over `postMessage` to the SW. The core caching, offline, invalidation, and auth work without them.
13
+
14
+ ## Compatible with everything
15
+
16
+ | Category | Examples | Notes |
17
+ |---|---|---|
18
+ | **Backend** | PHP, Laravel, Django, Rails, ASP.NET, Go, Java, Node | Serve `client-injector.js` as a static asset; zero backend integration needed |
19
+ | **HTML-over-wire** | HTMX, Turbo Hotwire, Unpoly, Livewire | SW caches HTML fragments by URL — body format is irrelevant |
20
+ | **SSG** | Astro, Hugo, 11ty, Jekyll | Precaches all built assets at install time; SW works even if no hydration occurs |
21
+ | **SPA frameworks** | React, Vue, Svelte, Solid, Angular, Alpine | Core SW + client utilities are plain JS; React gets generated hooks |
22
+ | **SSR meta-frameworks** | Next.js, Remix, Nuxt, SvelteKit, Analog | Intercepts `fetch()` transparently at the SW level; disable the framework's own SW |
23
+ | **Edge / Serverless** | Cloudflare Workers, Deno Deploy, Vercel Edge | Edge runs on CDN, Swoff runs in browser — independent layers, no conflict |
24
+ | **Islands / Resumability** | Astro islands, Qwik, Marko | SW runs in its own thread regardless of component hydration |
25
+
26
+ ## Why it matters
27
+
28
+ - **TanStack Query / SWR** — in-memory state only; no SW integration, no offline writes, no push
29
+ - **Workbox** — SW only; no data fetching, auth, or framework hooks
30
+ - **Apollo Client** — GraphQL only; no SW caching
31
+ - **Next.js / Remix** — full meta-frameworks dictating your entire architecture
32
+
33
+ Swoff is the only tool that combines SW caching, data fetching, offline writes, auth, real-time sync, and push into a single config-driven system — all framework-agnostic at the core, with optional reactive hooks per framework.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swoff/cli",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "type": "module",
5
5
  "description": "CLI for Swoff - Offline-first web apps made easy",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "files": [
20
20
  "bin/",
21
21
  "dist/",
22
+ "docs/",
22
23
  "templates/"
23
24
  ],
24
25
  "keywords": [
@@ -9,11 +9,11 @@ export function useAuth() {
9
9
  });
10
10
 
11
11
  useEffect(() => {
12
- getAuthState().then(setState);
12
+ getAuthState().then(setState).catch(() => {});
13
13
 
14
14
  const onOnline = () => setState((s) => ({ ...s, online: true }));
15
15
  const onOffline = () => setState((s) => ({ ...s, online: false }));
16
- const onAuthChange = () => getAuthState().then(setState);
16
+ const onAuthChange = () => getAuthState().then(setState).catch(() => {});
17
17
 
18
18
  window.addEventListener("online", onOnline);
19
19
  window.addEventListener("offline", onOffline);
@@ -9,11 +9,11 @@ export function useAuth() {
9
9
  });
10
10
 
11
11
  useEffect(() => {
12
- getAuthState().then(setState);
12
+ getAuthState().then(setState).catch(() => {});
13
13
 
14
14
  const onOnline = () => setState((s) => ({ ...s, online: true }));
15
15
  const onOffline = () => setState((s) => ({ ...s, online: false }));
16
- const onAuthChange = () => getAuthState().then(setState);
16
+ const onAuthChange = () => getAuthState().then(setState).catch(() => {});
17
17
 
18
18
  window.addEventListener("online", onOnline);
19
19
  window.addEventListener("offline", onOffline);
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback, useRef, startTransition } from "react";
1
+ import { useState, useEffect, useCallback, startTransition } from "react";
2
2
  import { fetchWithCache } from "../fetch-wrapper.js";
3
3
  import { generateTags } from "../invalidation-tags.js";
4
4
 
@@ -7,18 +7,26 @@ export function useCachedFetch(url, options = {}) {
7
7
  const [error, setError] = useState(null);
8
8
  const [loading, setLoading] = useState(true);
9
9
  const [refetchCount, setRefetchCount] = useState(0);
10
- const optionsRef = useRef(options);
11
- useEffect(() => { optionsRef.current = options; }, [options]);
12
10
 
13
11
  const refetch = useCallback(() => setRefetchCount((c) => c + 1), []);
14
12
 
13
+ const isEnabled = options.enabled !== false && url != null;
14
+
15
15
  useEffect(() => {
16
+ if (!isEnabled) {
17
+ startTransition(() => setLoading(false));
18
+ return;
19
+ }
16
20
  let cancelled = false;
21
+ const controller = new AbortController();
17
22
  startTransition(() => setLoading(true));
18
23
 
19
24
  const doFetch = async () => {
20
25
  try {
21
- const { response } = await fetchWithCache(url, optionsRef.current);
26
+ const { response } = await fetchWithCache(url, {
27
+ ...options,
28
+ signal: controller.signal,
29
+ });
22
30
  if (cancelled) return;
23
31
  if (response) {
24
32
  setData(await response.json());
@@ -27,6 +35,7 @@ export function useCachedFetch(url, options = {}) {
27
35
  }
28
36
  if (!cancelled) setError(null);
29
37
  } catch (err) {
38
+ if (!cancelled && err instanceof DOMException && err.name === "AbortError") return;
30
39
  if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));
31
40
  } finally {
32
41
  if (!cancelled) setLoading(false);
@@ -34,10 +43,15 @@ export function useCachedFetch(url, options = {}) {
34
43
  };
35
44
 
36
45
  doFetch();
37
- return () => { cancelled = true; };
38
- }, [url, refetchCount]);
46
+ return () => {
47
+ cancelled = true;
48
+ controller.abort();
49
+ };
50
+ }, [url, refetchCount, isEnabled]);
39
51
 
52
+ // Auto-refetch on cache invalidation
40
53
  useEffect(() => {
54
+ if (!isEnabled) return;
41
55
  const onInvalidated = (e) => {
42
56
  const detail = e.detail;
43
57
  const tags = detail?.tags;
@@ -49,7 +63,7 @@ export function useCachedFetch(url, options = {}) {
49
63
  };
50
64
  window.addEventListener("cache-invalidated", onInvalidated);
51
65
  return () => window.removeEventListener("cache-invalidated", onInvalidated);
52
- }, [url]);
66
+ }, [url, isEnabled]);
53
67
 
54
68
  return { data, error, loading, refetch };
55
69
  }