Skip to content

Commit 9e7aed8

Browse files
committed
feat: add cashe functional
1 parent de48137 commit 9e7aed8

7 files changed

Lines changed: 916 additions & 1 deletion

File tree

packages/playground/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import LoginForm from "./components/LoginForm.vue";
77
import {computed} from "vue";
88
import {tokenManager} from "@ametie/vue-muza-use";
99
import PromiseAllDemo from "./components/PromiseAllDemo.vue";
10+
import CacheDemo from "./components/CacheDemo.vue";
1011
1112
</script>
1213

1314
<template>
1415
<div>
1516
<login-form/>
1617
<promise-all-demo/>
18+
<cache-demo/>
1719
<!-- <PollingDemo />-->
1820
<!-- <hr style="margin: 40px 0; border: none; border-top: 1px solid #ddd;" />-->
1921

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
<script setup lang="ts">
2+
import { ref, onUnmounted } from 'vue'
3+
import { useApi, invalidateCache, clearAllCache } from '@ametie/vue-muza-use'
4+
5+
const STALE_TIME = 15_000 // 15 seconds — short enough to demo in browser
6+
7+
const requestCount = ref(0)
8+
const lastSource = ref<'network' | 'cache' | null>(null)
9+
const lastFetchedAt = ref<string | null>(null)
10+
11+
// Tracks seconds until cache expires
12+
const ttlSeconds = ref(0)
13+
let ttlTimer: ReturnType<typeof setInterval> | null = null
14+
15+
function startTtlCountdown() {
16+
if (ttlTimer) clearInterval(ttlTimer)
17+
ttlSeconds.value = Math.round(STALE_TIME / 1000)
18+
ttlTimer = setInterval(() => {
19+
if (ttlSeconds.value > 0) {
20+
ttlSeconds.value--
21+
} else {
22+
if (ttlTimer) clearInterval(ttlTimer)
23+
}
24+
}, 1000)
25+
}
26+
27+
onUnmounted(() => {
28+
if (ttlTimer) clearInterval(ttlTimer)
29+
})
30+
31+
const { data, loading, error, execute } = useApi<{ id: number; title: string }[]>('/lists', {
32+
cache: { id: 'demo-lists', staleTime: STALE_TIME },
33+
onSuccess: () => {
34+
requestCount.value++
35+
lastSource.value = 'network'
36+
lastFetchedAt.value = new Date().toLocaleTimeString()
37+
startTtlCountdown()
38+
},
39+
})
40+
41+
// Execute on mount and capture whether it was a cache hit
42+
async function fetchData() {
43+
const prevCount = requestCount.value
44+
await execute()
45+
// If requestCount didn't change, the response came from cache
46+
if (requestCount.value === prevCount && data.value) {
47+
lastSource.value = 'cache'
48+
}
49+
}
50+
51+
fetchData()
52+
53+
function forceRefetch() {
54+
clearAllCache()
55+
fetchData()
56+
}
57+
58+
function bustCache() {
59+
invalidateCache('demo-lists')
60+
fetchData()
61+
}
62+
</script>
63+
64+
<template>
65+
<div class="card">
66+
<h2>🗄️ Cache Demo</h2>
67+
<p class="description">
68+
Responses are cached for <strong>{{ STALE_TIME / 1000 }}s</strong>.
69+
Click <em>Refetch</em> to call <code>execute()</code> — the second call within the TTL
70+
is served instantly from cache without hitting the network.
71+
</p>
72+
73+
<div class="controls">
74+
<button @click="fetchData" :disabled="loading">
75+
{{ loading ? '⏳ Loading…' : '🔄 Refetch' }}
76+
</button>
77+
<button @click="forceRefetch" :disabled="loading" class="secondary">
78+
⚡ Force Network (clearAllCache)
79+
</button>
80+
<button @click="bustCache" :disabled="loading" class="secondary">
81+
🗑️ Invalidate Cache
82+
</button>
83+
</div>
84+
85+
<div class="stats">
86+
<div class="stat">
87+
<span class="label">Source</span>
88+
<span
89+
class="value badge"
90+
:class="lastSource === 'cache' ? 'badge-cache' : 'badge-network'"
91+
>
92+
{{ lastSource === 'cache' ? '⚡ Cache' : lastSource === 'network' ? '🌐 Network' : '—' }}
93+
</span>
94+
</div>
95+
<div class="stat">
96+
<span class="label">Network requests</span>
97+
<span class="value">{{ requestCount }}</span>
98+
</div>
99+
<div class="stat">
100+
<span class="label">Last fetched</span>
101+
<span class="value">{{ lastFetchedAt ?? '—' }}</span>
102+
</div>
103+
<div class="stat">
104+
<span class="label">Cache expires in</span>
105+
<span class="value" :class="{ dimmed: ttlSeconds === 0 }">
106+
{{ ttlSeconds > 0 ? `${ttlSeconds}s` : 'Expired' }}
107+
</span>
108+
</div>
109+
</div>
110+
111+
<div v-if="error" class="error">Error: {{ error.message }}</div>
112+
113+
<div v-if="data && data.length" class="data-preview">
114+
<h3>Data ({{ data.length }} items)</h3>
115+
<ul>
116+
<li v-for="item in data.slice(0, 5)" :key="item.id">
117+
<strong>#{{ item.id }}</strong> {{ item.title }}
118+
</li>
119+
<li v-if="data.length > 5" class="more">…and {{ data.length - 5 }} more</li>
120+
</ul>
121+
</div>
122+
</div>
123+
</template>
124+
125+
<style scoped>
126+
.card {
127+
border: 1px solid #e0e0e0;
128+
border-radius: 8px;
129+
padding: 24px;
130+
max-width: 600px;
131+
margin: 20px auto;
132+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
133+
background: #fff;
134+
color: #333;
135+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
136+
}
137+
138+
h2 {
139+
margin-top: 0;
140+
border-bottom: 2px solid #f0f0f0;
141+
padding-bottom: 10px;
142+
}
143+
144+
.description {
145+
color: #555;
146+
font-size: 0.9em;
147+
margin-bottom: 16px;
148+
}
149+
150+
.controls {
151+
display: flex;
152+
flex-wrap: wrap;
153+
gap: 10px;
154+
margin-bottom: 20px;
155+
}
156+
157+
button {
158+
padding: 8px 16px;
159+
background: #42b883;
160+
color: white;
161+
border: none;
162+
border-radius: 4px;
163+
cursor: pointer;
164+
font-weight: bold;
165+
font-size: 0.9em;
166+
}
167+
168+
button:hover:not(:disabled) {
169+
background: #33a06f;
170+
}
171+
172+
button:disabled {
173+
opacity: 0.5;
174+
cursor: not-allowed;
175+
}
176+
177+
button.secondary {
178+
background: #607d8b;
179+
}
180+
181+
button.secondary:hover:not(:disabled) {
182+
background: #455a64;
183+
}
184+
185+
.stats {
186+
display: grid;
187+
grid-template-columns: repeat(2, 1fr);
188+
gap: 12px;
189+
background: #f7f9fb;
190+
border-radius: 6px;
191+
padding: 16px;
192+
margin-bottom: 20px;
193+
}
194+
195+
.stat {
196+
display: flex;
197+
flex-direction: column;
198+
gap: 4px;
199+
}
200+
201+
.label {
202+
font-size: 0.75em;
203+
text-transform: uppercase;
204+
letter-spacing: 0.5px;
205+
color: #888;
206+
}
207+
208+
.value {
209+
font-size: 1.1em;
210+
font-weight: bold;
211+
color: #2c3e50;
212+
}
213+
214+
.value.dimmed {
215+
color: #aaa;
216+
}
217+
218+
.badge {
219+
display: inline-block;
220+
padding: 2px 10px;
221+
border-radius: 12px;
222+
font-size: 0.9em;
223+
}
224+
225+
.badge-cache {
226+
background: #e8f5e9;
227+
color: #2e7d32;
228+
}
229+
230+
.badge-network {
231+
background: #e3f2fd;
232+
color: #1565c0;
233+
}
234+
235+
.error {
236+
background: #ffebee;
237+
color: #c62828;
238+
padding: 10px;
239+
border-radius: 4px;
240+
margin-bottom: 10px;
241+
}
242+
243+
.data-preview {
244+
background: #fafafa;
245+
border: 1px solid #e0e0e0;
246+
border-radius: 6px;
247+
padding: 12px 16px;
248+
}
249+
250+
.data-preview h3 {
251+
margin: 0 0 8px;
252+
font-size: 0.9em;
253+
color: #555;
254+
}
255+
256+
ul {
257+
margin: 0;
258+
padding-left: 18px;
259+
}
260+
261+
li {
262+
margin-bottom: 4px;
263+
font-size: 0.9em;
264+
}
265+
266+
li.more {
267+
color: #888;
268+
list-style: none;
269+
margin-top: 4px;
270+
}
271+
</style>

0 commit comments

Comments
 (0)