pulse-js-framework 1.9.3 → 1.10.0
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/package.json +28 -1
- package/runtime/animation.js +535 -0
- package/runtime/dom-advanced.js +116 -37
- package/runtime/i18n.js +434 -0
- package/runtime/index.js +20 -0
- package/runtime/logger.js +5 -1
- package/runtime/persistence.js +492 -0
- package/runtime/sse.js +393 -0
- package/runtime/sw.js +250 -0
- package/sw/index.js +240 -0
package/sw/index.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Service Worker Utilities (SW Context)
|
|
3
|
+
* Cache strategy helpers for use INSIDE a service worker file.
|
|
4
|
+
*
|
|
5
|
+
* @module pulse-js-framework/sw
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // In your sw.js:
|
|
9
|
+
* import { createCacheStrategy } from 'pulse-js-framework/sw';
|
|
10
|
+
*
|
|
11
|
+
* const staticCache = createCacheStrategy('cache-first', {
|
|
12
|
+
* cacheName: 'static-v1',
|
|
13
|
+
* match: /\.(js|css|png|woff2)$/,
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* self.addEventListener('fetch', (event) => {
|
|
17
|
+
* staticCache.handle(event);
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// CACHE STRATEGY FACTORY
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a cache strategy for handling fetch events
|
|
27
|
+
*
|
|
28
|
+
* @param {'cache-first'|'network-first'|'stale-while-revalidate'|'network-only'|'cache-only'} name - Strategy type
|
|
29
|
+
* @param {Object} options - Strategy options
|
|
30
|
+
* @param {string} options.cacheName - Cache storage name
|
|
31
|
+
* @param {RegExp|Function} [options.match] - URL matching (regex or function)
|
|
32
|
+
* @param {number} [options.maxAge] - Max cache age in ms
|
|
33
|
+
* @param {number} [options.maxEntries] - Max cached entries
|
|
34
|
+
* @param {number} [options.timeout] - Network timeout in ms (for network-first)
|
|
35
|
+
* @returns {Object} Strategy with handle() method
|
|
36
|
+
*/
|
|
37
|
+
export function createCacheStrategy(name, options = {}) {
|
|
38
|
+
const {
|
|
39
|
+
cacheName,
|
|
40
|
+
match = null,
|
|
41
|
+
maxAge = 0,
|
|
42
|
+
maxEntries = 0,
|
|
43
|
+
timeout = 5000,
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
if (!cacheName) {
|
|
47
|
+
throw new Error('createCacheStrategy: cacheName is required');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function _matches(request) {
|
|
51
|
+
const url = request.url || request;
|
|
52
|
+
if (!match) return true;
|
|
53
|
+
if (match instanceof RegExp) return match.test(url);
|
|
54
|
+
if (typeof match === 'function') return match(url);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function _cleanupCache(cache) {
|
|
59
|
+
if (!maxEntries && !maxAge) return;
|
|
60
|
+
|
|
61
|
+
const keys = await cache.keys();
|
|
62
|
+
|
|
63
|
+
if (maxAge > 0) {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
for (const request of keys) {
|
|
66
|
+
const response = await cache.match(request);
|
|
67
|
+
if (response) {
|
|
68
|
+
const dateHeader = response.headers.get('sw-cache-time');
|
|
69
|
+
if (dateHeader && (now - parseInt(dateHeader, 10)) > maxAge) {
|
|
70
|
+
await cache.delete(request);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (maxEntries > 0) {
|
|
77
|
+
const remaining = await cache.keys();
|
|
78
|
+
if (remaining.length > maxEntries) {
|
|
79
|
+
const toDelete = remaining.slice(0, remaining.length - maxEntries);
|
|
80
|
+
for (const request of toDelete) {
|
|
81
|
+
await cache.delete(request);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function _cacheResponse(cache, request, response) {
|
|
88
|
+
// Clone and add cache timestamp header
|
|
89
|
+
const headers = new Headers(response.headers);
|
|
90
|
+
headers.set('sw-cache-time', String(Date.now()));
|
|
91
|
+
|
|
92
|
+
const cachedResponse = new Response(response.clone().body, {
|
|
93
|
+
status: response.status,
|
|
94
|
+
statusText: response.statusText,
|
|
95
|
+
headers,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await cache.put(request, cachedResponse);
|
|
99
|
+
_cleanupCache(cache).catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Strategy implementations
|
|
103
|
+
const strategies = {
|
|
104
|
+
'cache-first': async (request) => {
|
|
105
|
+
const cache = await caches.open(cacheName);
|
|
106
|
+
const cached = await cache.match(request);
|
|
107
|
+
if (cached) return cached;
|
|
108
|
+
|
|
109
|
+
const response = await fetch(request);
|
|
110
|
+
if (response.ok) {
|
|
111
|
+
await _cacheResponse(cache, request, response);
|
|
112
|
+
}
|
|
113
|
+
return response;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
'network-first': async (request) => {
|
|
117
|
+
const cache = await caches.open(cacheName);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
122
|
+
|
|
123
|
+
const response = await fetch(request, { signal: controller.signal });
|
|
124
|
+
clearTimeout(timeoutId);
|
|
125
|
+
|
|
126
|
+
if (response.ok) {
|
|
127
|
+
await _cacheResponse(cache, request, response);
|
|
128
|
+
}
|
|
129
|
+
return response;
|
|
130
|
+
} catch {
|
|
131
|
+
const cached = await cache.match(request);
|
|
132
|
+
if (cached) return cached;
|
|
133
|
+
throw new Error(`Network request failed and no cache for: ${request.url}`);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
'stale-while-revalidate': async (request) => {
|
|
138
|
+
const cache = await caches.open(cacheName);
|
|
139
|
+
const cached = await cache.match(request);
|
|
140
|
+
|
|
141
|
+
// Revalidate in background
|
|
142
|
+
const fetchPromise = fetch(request).then(response => {
|
|
143
|
+
if (response.ok) {
|
|
144
|
+
_cacheResponse(cache, request, response).catch(() => {});
|
|
145
|
+
}
|
|
146
|
+
return response;
|
|
147
|
+
}).catch(() => null);
|
|
148
|
+
|
|
149
|
+
// Return cached immediately, or wait for network
|
|
150
|
+
return cached || fetchPromise;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
'network-only': async (request) => {
|
|
154
|
+
return fetch(request);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
'cache-only': async (request) => {
|
|
158
|
+
const cache = await caches.open(cacheName);
|
|
159
|
+
const cached = await cache.match(request);
|
|
160
|
+
if (cached) return cached;
|
|
161
|
+
throw new Error(`No cache entry for: ${request.url}`);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const strategyFn = strategies[name];
|
|
166
|
+
if (!strategyFn) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Unknown cache strategy: "${name}". Use: cache-first, network-first, stale-while-revalidate, network-only, cache-only`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
name,
|
|
174
|
+
cacheName,
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handle a fetch event with this strategy
|
|
178
|
+
* @param {FetchEvent} event - The fetch event
|
|
179
|
+
* @returns {boolean} True if this strategy handled the event
|
|
180
|
+
*/
|
|
181
|
+
handle(event) {
|
|
182
|
+
if (!_matches(event.request)) return false;
|
|
183
|
+
|
|
184
|
+
event.respondWith(strategyFn(event.request));
|
|
185
|
+
return true;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Fetch using this strategy (without FetchEvent)
|
|
190
|
+
* @param {Request|string} request - The request
|
|
191
|
+
* @returns {Promise<Response>}
|
|
192
|
+
*/
|
|
193
|
+
fetch(request) {
|
|
194
|
+
return strategyFn(typeof request === 'string' ? new Request(request) : request);
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Precache a list of URLs
|
|
199
|
+
* @param {string[]} urls - URLs to precache
|
|
200
|
+
* @returns {Promise<void>}
|
|
201
|
+
*/
|
|
202
|
+
async precache(urls) {
|
|
203
|
+
const cache = await caches.open(cacheName);
|
|
204
|
+
await cache.addAll(urls);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Clear this strategy's cache
|
|
209
|
+
* @returns {Promise<boolean>}
|
|
210
|
+
*/
|
|
211
|
+
async clearCache() {
|
|
212
|
+
return caches.delete(cacheName);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// SKIP WAITING LISTENER
|
|
219
|
+
// =============================================================================
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Install a message listener for SKIP_WAITING messages.
|
|
223
|
+
* Call this in your service worker to enable skipWaiting from the main thread.
|
|
224
|
+
*/
|
|
225
|
+
export function enableSkipWaiting() {
|
|
226
|
+
self.addEventListener('message', (event) => {
|
|
227
|
+
if (event.data?.type === 'SKIP_WAITING') {
|
|
228
|
+
self.skipWaiting();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// DEFAULT EXPORT
|
|
235
|
+
// =============================================================================
|
|
236
|
+
|
|
237
|
+
export default {
|
|
238
|
+
createCacheStrategy,
|
|
239
|
+
enableSkipWaiting,
|
|
240
|
+
};
|