@reidelsaltres/pureper 0.2.15 → 0.2.18

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 (40) hide show
  1. package/out/foundation/Fetcher.d.ts.map +1 -1
  2. package/out/foundation/Fetcher.js +8 -13
  3. package/out/foundation/Fetcher.js.map +1 -1
  4. package/out/foundation/Injection.d.ts +87 -0
  5. package/out/foundation/Injection.d.ts.map +1 -0
  6. package/out/foundation/Injection.js +149 -0
  7. package/out/foundation/Injection.js.map +1 -0
  8. package/out/foundation/Triplet.d.ts +30 -25
  9. package/out/foundation/Triplet.d.ts.map +1 -1
  10. package/out/foundation/Triplet.js +96 -115
  11. package/out/foundation/Triplet.js.map +1 -1
  12. package/out/foundation/TripletDecorator.d.ts +11 -0
  13. package/out/foundation/TripletDecorator.d.ts.map +1 -1
  14. package/out/foundation/TripletDecorator.js +25 -8
  15. package/out/foundation/TripletDecorator.js.map +1 -1
  16. package/out/foundation/component_api/UniHtml.d.ts +5 -1
  17. package/out/foundation/component_api/UniHtml.d.ts.map +1 -1
  18. package/out/foundation/component_api/UniHtml.js +23 -5
  19. package/out/foundation/component_api/UniHtml.js.map +1 -1
  20. package/out/foundation/worker/ServiceWorker.d.ts +48 -17
  21. package/out/foundation/worker/ServiceWorker.d.ts.map +1 -1
  22. package/out/foundation/worker/ServiceWorker.js +186 -119
  23. package/out/foundation/worker/ServiceWorker.js.map +1 -1
  24. package/out/index.d.ts +4 -3
  25. package/out/index.d.ts.map +1 -1
  26. package/out/index.js +3 -2
  27. package/out/index.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/foundation/Fetcher.ts +9 -14
  30. package/src/foundation/Injection.ts +183 -0
  31. package/src/foundation/Triplet.ts +114 -141
  32. package/src/foundation/TripletDecorator.ts +32 -8
  33. package/src/foundation/component_api/UniHtml.ts +26 -5
  34. package/src/foundation/worker/ServiceWorker.ts +203 -129
  35. package/src/foundation/worker/serviceworker.js +191 -0
  36. package/src/index.ts +8 -5
  37. package/out/foundation/worker/serviceworker.d.ts +0 -1
  38. package/out/foundation/worker/serviceworker.d.ts.map +0 -1
  39. package/out/foundation/worker/serviceworker.js +0 -2
  40. package/out/foundation/worker/serviceworker.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { AnyConstructor, UniHtml } from "../index.js";
2
- import Triplet, { AccessType, TripletStruct } from "./Triplet.js"
2
+ import Triplet, { TripletStruct } from "./Triplet.js"
3
3
 
4
4
  export function ReComponent(settings: TripletStruct, tag: string) {
5
5
  return (ctor: Function) => {
@@ -12,9 +12,6 @@ export function ReComponent(settings: TripletStruct, tag: string) {
12
12
  const triplet: Triplet = new Triplet(settings);
13
13
 
14
14
  triplet.register("markup", tag)
15
- .then(ok => {
16
- if (!ok) console.error(`[ReComponent:${tag}] registration returned false`);
17
- })
18
15
  .catch(err => console.error(`[ReComponent:${tag}] register failed`, err));
19
16
  }
20
17
  }
@@ -25,13 +22,40 @@ export function RePage(settings: TripletStruct, route: string) {
25
22
 
26
23
  if (settings.class === null || settings.class === undefined)
27
24
  settings.class = ctor as AnyConstructor<UniHtml>;
28
-
25
+
29
26
  const triplet: Triplet = new Triplet(settings);
30
27
 
31
28
  triplet.register("router", route)
32
- .then(ok => {
33
- if (!ok) console.error(`[RePage:${route}] registration returned false`);
34
- })
35
29
  .catch(err => console.error(`[RePage:${route}] register failed`, err));
36
30
  }
31
+ }
32
+
33
+ /**
34
+ * Register an alternative implementation for an existing placeholder.
35
+ *
36
+ * ```ts
37
+ * @ReImplementation({ markupURL: './Fancy.hmle', cssURL: './Fancy.css' }, "re-button")
38
+ * class FancyButton extends Component { ... }
39
+ *
40
+ * // Then switch: Placeholder.switchTo("re-button", "FancyButton");
41
+ * ```
42
+ */
43
+ export function ReImplementation(settings: TripletStruct, target: string) {
44
+ return (ctor: Function) => {
45
+ if (target == null || target.length === 0)
46
+ throw new Error("Invalid implementation target.");
47
+
48
+ if (settings.class === null || settings.class === undefined)
49
+ settings.class = ctor as AnyConstructor<UniHtml>;
50
+
51
+ // Use the class name as the implementation name
52
+ const implName = ctor.name;
53
+ const triplet: Triplet = new Triplet(settings, implName);
54
+
55
+ // Register adds the implementation to the existing placeholder (or creates one)
56
+ triplet.register(target.includes("-") ? "markup" : "router", target)
57
+ .catch(err => console.error(`[ReImplementation:${target}] register failed`, err));
58
+
59
+ console.info(`[ReImplementation:${target}] registered implementation "${implName}"`);
60
+ }
37
61
  }
@@ -17,10 +17,10 @@ export default class UniHtml {
17
17
  */
18
18
  public async load(element: HTMLElement | ShadowRoot): Promise<void> {
19
19
  this._status.setObject("loading");
20
+ (this as any)._lastRenderTarget = element;
20
21
  await this.preInit();
21
22
 
22
- const preHtml: TemplateHolder = await this._init();
23
- const html: TemplateHolder = await this._postInit(preHtml);
23
+ const html: TemplateHolder = await this._init();
24
24
 
25
25
  // ВАЖНО: preLoad() вызывается ДО монтирования в DOM/Shadow DOM.
26
26
  // Для компонентов (UniHtmlComponent) на этом этапе ещё нельзя полагаться на this.shadowRoot —
@@ -37,9 +37,6 @@ export default class UniHtml {
37
37
  this._status.setObject("ready");
38
38
  }
39
39
 
40
- private async _postInit(html: TemplateHolder): Promise<TemplateHolder> {
41
- throw new Error("Method not implemented.");
42
- }
43
40
  private async _init(): Promise<TemplateHolder> {
44
41
  throw new Error("Method not implemented.");
45
42
  }
@@ -99,4 +96,28 @@ export default class UniHtml {
99
96
  this._templateHolder = undefined;
100
97
  }
101
98
  }
99
+
100
+ /**
101
+ * Reload this instance: dispose current state, then re-run the full lifecycle.
102
+ * Called automatically when the active Implementation is switched via Placeholder.
103
+ */
104
+ public async reload(): Promise<void> {
105
+ const renderTarget = (this as any).shadowRoot ?? (this as any)._lastRenderTarget;
106
+ await this.dispose();
107
+ this._status.setObject("constructed");
108
+
109
+ if (renderTarget) {
110
+ // Clear existing content
111
+ while (renderTarget.firstChild) {
112
+ renderTarget.removeChild(renderTarget.firstChild);
113
+ }
114
+ // Clear adopted stylesheets so the new implementation applies its own
115
+ if ('adoptedStyleSheets' in renderTarget) {
116
+ renderTarget.adoptedStyleSheets = [];
117
+ }
118
+ await this.load(renderTarget);
119
+ }
120
+
121
+ console.info(`[UniHtml]: Instance reloaded`);
122
+ }
102
123
  }
@@ -1,155 +1,229 @@
1
- /**
2
- * Service Worker for Pureper SPA
3
- * Handles client-side routing by intercepting navigation requests
4
- * and serving index.html for all SPA routes
5
- */
6
- import type { ExtendableEvent } from './api/ExtendableEvent.js';
7
- import type { FetchEvent } from './api/FetchEvent.js';
8
- import type { ServiceWorkerGlobalScope } from './api/ServiceWorkerGlobalScope.js';
9
-
10
- // Type assertion for Service Worker context
11
- declare let self: ServiceWorkerGlobalScope
12
- const swSelf = self;
13
-
14
- // Автоматически генерируем CACHE_NAME из base.json
15
- //import base from '../../../data/base.json';
16
- const CACHE_NAME = `pureper-v1`;
17
-
18
- const STATIC_ASSETS: string[] = [
19
- '/index.html'
20
- ];
21
-
22
- /*if ('serviceWorker' in navigator) {
23
- window.addEventListener('load', () => {
24
- navigator.serviceWorker.register('./serviceWorker.js', { type: 'module' })
25
- .then((registration) => {
26
- console.log('ServiceWorker registration successful:', registration.scope);
27
- })
28
- .catch((error) => {
29
- console.error('ServiceWorker registration failed:', error);
30
- });
31
- });
32
- window.addEventListener('fetch', (event: FetchEvent) => {
33
- event.respondWith(
34
- caches.match(event.request).then((cachedResponse) => {
35
- console.log(`[ServiceWorker]: Fetching ${event.request.url}`);
36
- return cachedResponse || fetch(event.request);
37
- })
38
- );
39
- });
40
- }
1
+ import Observable from "../api/Observer.js";
2
+
3
+ export type ServiceWorkerConfig = {
4
+ scriptURL?: string;
5
+ scope?: string;
6
+ };
41
7
 
42
8
  /**
43
- * Install event - cache static assets
9
+ * Client-side Service Worker manager for Purper SPA.
10
+ *
11
+ * Provides:
12
+ * - Registration with one call (`ServiceWorker.register()`)
13
+ * - Cache management: add / remove / list / clear
14
+ * - Connectivity detection with reactive `online` observable
15
+ *
16
+ * Usage:
17
+ * ```ts
18
+ * await ServiceWorker.register(); // defaults to './serviceworker.js'
19
+ * await ServiceWorker.addToCache('/data.json');
20
+ * await ServiceWorker.removeFromCache('/old.css');
21
+ * ServiceWorker.online.subscribe(v => console.log('online:', v));
22
+ * ```
44
23
  */
45
- /*window.addEventListener('install', (event: ExtendableEvent) => {
46
- console.log('ServiceWorker: Installing...');
47
- const assetsToCache = [
48
- ...STATIC_ASSETS
49
- ];
50
- // Remove duplicates
51
- const uniqueAssets = Array.from(new Set(assetsToCache));
52
- event.waitUntil(
53
- caches.open(CACHE_NAME)
54
- .then((cache: Cache) => {
55
- console.log('ServiceWorker: Caching static assets and SPA routes');
56
- return cache.addAll(uniqueAssets);
57
- })
58
- .then(() => {
59
- console.log('ServiceWorker: Installation complete');
60
- return swSelf.skipWaiting();
61
- })
62
- );
63
- });*/
64
-
65
24
  export default class ServiceWorker {
66
- /**
67
- * Sends a message to the service worker to cache a specific URL.
68
- * @param url The URL of the resource to cache.
69
- */
70
- static async addToCache(url: string): Promise<void> {
71
- if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
72
- navigator.serviceWorker.controller.postMessage({
73
- type: 'CACHE_URL',
74
- url: url
25
+ private static _registration?: ServiceWorkerRegistration;
26
+
27
+ /** Observable connectivity state subscribe for real-time changes. */
28
+ static readonly online: Observable<boolean> = new Observable(navigator.onLine);
29
+
30
+ // ── Connectivity listeners (bound once) ─────────────────────────
31
+ private static _connectivityBound = false;
32
+ private static _bindConnectivity(): void {
33
+ if (this._connectivityBound) return;
34
+ this._connectivityBound = true;
35
+
36
+ window.addEventListener('online', () => {
37
+ console.log('[ServiceWorker]: Browser went online');
38
+ this.online.setObject(true);
39
+ });
40
+ window.addEventListener('offline', () => {
41
+ console.log('[ServiceWorker]: Browser went offline');
42
+ this.online.setObject(false);
43
+ });
44
+ }
45
+
46
+ // ── Registration ────────────────────────────────────────────────
47
+ static async register(config?: ServiceWorkerConfig): Promise<ServiceWorkerRegistration | undefined> {
48
+ if (!('serviceWorker' in navigator)) {
49
+ console.warn('[ServiceWorker]: Service Workers are not supported in this browser');
50
+ return undefined;
51
+ }
52
+
53
+ this._bindConnectivity();
54
+
55
+ const scriptURL = config?.scriptURL ?? './serviceworker.js';
56
+ const scope = config?.scope ?? '/';
57
+
58
+ try {
59
+ const reg = await navigator.serviceWorker.register(scriptURL, { scope });
60
+ this._registration = reg;
61
+ console.log(`[ServiceWorker]: Registered "${scriptURL}" with scope "${reg.scope}"`);
62
+
63
+ reg.addEventListener('updatefound', () => {
64
+ const newWorker = reg.installing;
65
+ if (!newWorker) return;
66
+ console.log('[ServiceWorker]: New version installing...');
67
+ newWorker.addEventListener('statechange', () => {
68
+ if (newWorker.state === 'activated') {
69
+ console.log('[ServiceWorker]: New version activated');
70
+ }
71
+ });
75
72
  });
76
- } else {
77
- // Fallback: try to cache directly using the Cache API
78
- try {
79
- const cache = await caches.open('pureper-v1');
80
- const response = await fetch(url);
81
- if (response.ok) {
82
- await cache.put(url, response);
83
- console.log('[ServiceWorker]: Resource cached directly:', url);
84
- }
85
- } catch (e) {
86
- console.warn('[ServiceWorker]: Failed to cache resource:', url, e);
73
+
74
+ // Wait for the controller to be available
75
+ if (!navigator.serviceWorker.controller) {
76
+ await new Promise<void>((resolve) => {
77
+ navigator.serviceWorker.addEventListener('controllerchange', () => resolve(), { once: true });
78
+ });
87
79
  }
80
+
81
+ return reg;
82
+ } catch (err) {
83
+ console.error('[ServiceWorker]: Registration failed', err);
84
+ return undefined;
88
85
  }
89
86
  }
90
87
 
91
- /**
92
- * Asks the service worker to skip waiting and activate the new version.
93
- */
94
- static skipWaiting(): void {
95
- if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
96
- navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' });
97
- }
88
+ // ── Helpers ─────────────────────────────────────────────────────
89
+ private static _postMessage(msg: object): void {
90
+ navigator.serviceWorker?.controller?.postMessage(msg);
98
91
  }
99
92
 
100
- /**
101
- * Gets the version of the currently active service worker.
102
- * @returns A promise that resolves with the version string.
103
- */
104
- static getVersion(): Promise<string> {
93
+ private static _request<T>(msg: object): Promise<T> {
105
94
  return new Promise((resolve, reject) => {
106
- if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
107
- const messageChannel = new MessageChannel();
108
- messageChannel.port1.onmessage = (event) => {
109
- if (event.data.error) {
110
- reject(event.data.error);
111
- } else {
112
- resolve(event.data.version);
113
- }
114
- };
115
- navigator.serviceWorker.controller.postMessage({ type: 'GET_VERSION' }, [messageChannel.port2]);
116
- } else {
117
- reject('Service worker not available.');
95
+ if (!navigator.serviceWorker?.controller) {
96
+ reject('[ServiceWorker]: No active controller');
97
+ return;
118
98
  }
99
+ const mc = new MessageChannel();
100
+ mc.port1.onmessage = (ev) => resolve(ev.data as T);
101
+ navigator.serviceWorker.controller.postMessage(msg, [mc.port2]);
119
102
  });
120
103
  }
121
104
 
122
- /**
123
- * Checks if the given URL is present in the Service Worker's cache.
124
- */
125
- static isCached(url: string): Promise<boolean> {
126
- return new Promise((resolve) => {
127
- if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
128
- const mc = new MessageChannel();
129
- mc.port1.onmessage = (ev) => {
130
- if (ev.data && typeof ev.data.cached === 'boolean') {
131
- resolve(ev.data.cached);
132
- } else {
133
- resolve(false);
134
- }
135
- };
136
- navigator.serviceWorker.controller.postMessage({ type: 'HAS_URL', url }, [mc.port2]);
137
- } else {
138
- // Fallback: try CacheStorage directly (same-origin only)
139
- caches.match(url).then(match => resolve(!!match)).catch(() => resolve(false));
105
+ // ── Cache Management ────────────────────────────────────────────
106
+
107
+ /** Add a single URL to the SW cache. */
108
+ static async addToCache(url: string): Promise<void> {
109
+ if (navigator.serviceWorker?.controller) {
110
+ this._postMessage({ type: 'CACHE_URL', url });
111
+ return;
112
+ }
113
+ // Fallback: use Cache API directly
114
+ try {
115
+ const cache = await caches.open('purper-v1');
116
+ const response = await fetch(url);
117
+ if (response.ok) {
118
+ await cache.put(url, response);
119
+ console.log('[ServiceWorker]: Resource cached directly:', url);
140
120
  }
141
- });
121
+ } catch (e) {
122
+ console.warn('[ServiceWorker]: Failed to cache resource:', url, e);
123
+ }
142
124
  }
143
125
 
126
+ /** Add multiple URLs to the SW cache in one batch. */
127
+ static addAllToCache(urls: string[]): void {
128
+ this._postMessage({ type: 'CACHE_URLS', urls });
129
+ }
130
+
131
+ /** Remove a URL from the SW cache. Returns true if it was found and deleted. */
132
+ static async removeFromCache(url: string): Promise<boolean> {
133
+ try {
134
+ const data = await this._request<{ deleted: boolean }>({ type: 'REMOVE_URL', url });
135
+ return data.deleted;
136
+ } catch {
137
+ // Fallback
138
+ try {
139
+ const cache = await caches.open('purper-v1');
140
+ return await cache.delete(url);
141
+ } catch {
142
+ return false;
143
+ }
144
+ }
145
+ }
146
+
147
+ /** Return all URLs currently in the SW cache. */
148
+ static async getCacheKeys(): Promise<string[]> {
149
+ try {
150
+ const data = await this._request<{ keys: string[] }>({ type: 'GET_CACHE_KEYS' });
151
+ return data.keys;
152
+ } catch {
153
+ return [];
154
+ }
155
+ }
156
+
157
+ /** Wipe the entire SW cache. */
158
+ static async clearCache(): Promise<boolean> {
159
+ try {
160
+ const data = await this._request<{ cleared: boolean }>({ type: 'CLEAR_CACHE' });
161
+ return data.cleared;
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+
167
+ /** Check whether a URL exists in the SW cache. */
168
+ static async isCached(url: string): Promise<boolean> {
169
+ try {
170
+ const data = await this._request<{ cached: boolean }>({ type: 'HAS_URL', url });
171
+ return data.cached;
172
+ } catch {
173
+ // Fallback: CacheStorage directly
174
+ try {
175
+ return !!(await caches.match(url));
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+ }
181
+
182
+ // ── Version & lifecycle ─────────────────────────────────────────
183
+
184
+ /** Ask the waiting SW to activate immediately. */
185
+ static skipWaiting(): void {
186
+ this._postMessage({ type: 'SKIP_WAITING' });
187
+ }
188
+
189
+ /** Get the version string from the running SW. */
190
+ static async getVersion(): Promise<string> {
191
+ const data = await this._request<{ version: string }>({ type: 'GET_VERSION' });
192
+ return data.version;
193
+ }
194
+
195
+ // ── Connectivity ────────────────────────────────────────────────
196
+
144
197
  /**
145
- * Checks if the browser is online.
198
+ * Active connectivity probe makes a real network request.
199
+ * Unlike `online` observable (which relies on browser events), this
200
+ * detects captive portals and lie-fi.
146
201
  */
147
- static async isOnline(): Promise<boolean> {
202
+ static async isOnline(probeURL?: string): Promise<boolean> {
148
203
  try {
149
- const response = await fetch('./index.html', { cache: 'no-store' });
150
- return response && response.ok;
204
+ // Try SW-side probe first
205
+ const data = await this._request<{ online: boolean }>({
206
+ type: 'IS_ONLINE',
207
+ url: probeURL ?? '/index.html'
208
+ });
209
+ this.online.setObject(data.online);
210
+ return data.online;
151
211
  } catch {
152
- return false;
212
+ // Fallback: client-side probe
213
+ try {
214
+ const response = await fetch(probeURL ?? './index.html', { cache: 'no-store', method: 'HEAD' });
215
+ const result = response.ok;
216
+ this.online.setObject(result);
217
+ return result;
218
+ } catch {
219
+ this.online.setObject(false);
220
+ return false;
221
+ }
153
222
  }
154
223
  }
224
+
225
+ /** Current registration, if any. */
226
+ static get registration(): ServiceWorkerRegistration | undefined {
227
+ return this._registration;
228
+ }
155
229
  }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Purper Service Worker
3
+ * Handles caching, offline support, and connectivity detection.
4
+ *
5
+ * Message API (postMessage from client):
6
+ * CACHE_URL { url } — add a URL to cache
7
+ * CACHE_URLS { urls } — add multiple URLs to cache
8
+ * REMOVE_URL { url } — remove a URL from cache
9
+ * CLEAR_CACHE — — wipe entire cache
10
+ * GET_CACHE_KEYS — — list all cached URLs
11
+ * HAS_URL { url } — check if URL is cached
12
+ * SKIP_WAITING — — activate new SW immediately
13
+ * GET_VERSION — — return SW version string
14
+ * IS_ONLINE — — connectivity check from SW context
15
+ */
16
+
17
+ const CACHE_VERSION = 'purper-v1';
18
+ const SW_VERSION = '1.0.0';
19
+
20
+ // ── Install ─────────────────────────────────────────────────────────
21
+ self.addEventListener('install', (event) => {
22
+ console.log(`[ServiceWorker ${SW_VERSION}]: Installing...`);
23
+ event.waitUntil(
24
+ caches.open(CACHE_VERSION)
25
+ .then(() => {
26
+ console.log(`[ServiceWorker ${SW_VERSION}]: Cache "${CACHE_VERSION}" opened`);
27
+ return self.skipWaiting();
28
+ })
29
+ );
30
+ });
31
+
32
+ // ── Activate ────────────────────────────────────────────────────────
33
+ self.addEventListener('activate', (event) => {
34
+ console.log(`[ServiceWorker ${SW_VERSION}]: Activating...`);
35
+ event.waitUntil(
36
+ caches.keys()
37
+ .then((keys) => {
38
+ return Promise.all(
39
+ keys
40
+ .filter((key) => key !== CACHE_VERSION)
41
+ .map((key) => {
42
+ console.log(`[ServiceWorker ${SW_VERSION}]: Deleting old cache "${key}"`);
43
+ return caches.delete(key);
44
+ })
45
+ );
46
+ })
47
+ .then(() => {
48
+ console.log(`[ServiceWorker ${SW_VERSION}]: Claiming clients`);
49
+ return self.clients.claim();
50
+ })
51
+ );
52
+ });
53
+
54
+ // ── Fetch ───────────────────────────────────────────────────────────
55
+ // Network-first for navigation, cache-first for assets.
56
+ self.addEventListener('fetch', (event) => {
57
+ const request = event.request;
58
+
59
+ // Only handle GET requests
60
+ if (request.method !== 'GET') return;
61
+
62
+ // Navigation requests (HTML pages) — network-first, fall back to cache
63
+ if (request.mode === 'navigate') {
64
+ event.respondWith(
65
+ fetch(request)
66
+ .then((response) => {
67
+ if (response.ok) {
68
+ const clone = response.clone();
69
+ caches.open(CACHE_VERSION).then((cache) => cache.put(request, clone));
70
+ }
71
+ return response;
72
+ })
73
+ .catch(() => {
74
+ return caches.match(request).then((cached) => {
75
+ return cached || caches.match('/index.html');
76
+ });
77
+ })
78
+ );
79
+ return;
80
+ }
81
+
82
+ // Sub-resources (CSS, JS, images, fonts, JSON) — cache-first, fall back to network
83
+ event.respondWith(
84
+ caches.match(request).then((cached) => {
85
+ if (cached) return cached;
86
+
87
+ return fetch(request).then((response) => {
88
+ if (response.ok && response.type === 'basic') {
89
+ const clone = response.clone();
90
+ caches.open(CACHE_VERSION).then((cache) => cache.put(request, clone));
91
+ }
92
+ return response;
93
+ });
94
+ })
95
+ );
96
+ });
97
+
98
+ // ── Message handling ────────────────────────────────────────────────
99
+ self.addEventListener('message', (event) => {
100
+ const { type, url, urls } = event.data || {};
101
+ const port = event.ports?.[0];
102
+
103
+ switch (type) {
104
+ case 'CACHE_URL':
105
+ caches.open(CACHE_VERSION)
106
+ .then((cache) => cache.add(url))
107
+ .then(() => console.log(`[ServiceWorker]: Cached "${url}"`))
108
+ .catch((err) => console.warn(`[ServiceWorker]: Failed to cache "${url}"`, err));
109
+ break;
110
+
111
+ case 'CACHE_URLS':
112
+ if (Array.isArray(urls)) {
113
+ caches.open(CACHE_VERSION)
114
+ .then((cache) => cache.addAll(urls))
115
+ .then(() => console.log(`[ServiceWorker]: Cached ${urls.length} URLs`))
116
+ .catch((err) => console.warn('[ServiceWorker]: Failed to cache URLs', err));
117
+ }
118
+ break;
119
+
120
+ case 'REMOVE_URL':
121
+ caches.open(CACHE_VERSION)
122
+ .then((cache) => cache.delete(url))
123
+ .then((deleted) => {
124
+ console.log(`[ServiceWorker]: ${deleted ? 'Removed' : 'Not found'} "${url}" from cache`);
125
+ if (port) port.postMessage({ deleted });
126
+ })
127
+ .catch((err) => {
128
+ console.warn(`[ServiceWorker]: Failed to remove "${url}"`, err);
129
+ if (port) port.postMessage({ deleted: false });
130
+ });
131
+ break;
132
+
133
+ case 'CLEAR_CACHE':
134
+ caches.delete(CACHE_VERSION)
135
+ .then(() => caches.open(CACHE_VERSION))
136
+ .then(() => {
137
+ console.log('[ServiceWorker]: Cache cleared');
138
+ if (port) port.postMessage({ cleared: true });
139
+ })
140
+ .catch((err) => {
141
+ console.warn('[ServiceWorker]: Failed to clear cache', err);
142
+ if (port) port.postMessage({ cleared: false });
143
+ });
144
+ break;
145
+
146
+ case 'GET_CACHE_KEYS':
147
+ caches.open(CACHE_VERSION)
148
+ .then((cache) => cache.keys())
149
+ .then((requests) => {
150
+ const keys = requests.map((r) => r.url);
151
+ if (port) port.postMessage({ keys });
152
+ })
153
+ .catch(() => {
154
+ if (port) port.postMessage({ keys: [] });
155
+ });
156
+ break;
157
+
158
+ case 'HAS_URL':
159
+ caches.match(url)
160
+ .then((match) => {
161
+ if (port) port.postMessage({ cached: !!match });
162
+ })
163
+ .catch(() => {
164
+ if (port) port.postMessage({ cached: false });
165
+ });
166
+ break;
167
+
168
+ case 'SKIP_WAITING':
169
+ console.log('[ServiceWorker]: Skip waiting requested');
170
+ self.skipWaiting();
171
+ break;
172
+
173
+ case 'GET_VERSION':
174
+ if (port) port.postMessage({ version: SW_VERSION });
175
+ break;
176
+
177
+ case 'IS_ONLINE': {
178
+ fetch(url || '/index.html', { cache: 'no-store', method: 'HEAD' })
179
+ .then((res) => {
180
+ if (port) port.postMessage({ online: res.ok });
181
+ })
182
+ .catch(() => {
183
+ if (port) port.postMessage({ online: false });
184
+ });
185
+ break;
186
+ }
187
+
188
+ default:
189
+ console.warn(`[ServiceWorker]: Unknown message type "${type}"`);
190
+ }
191
+ });