@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.
- package/out/foundation/Fetcher.d.ts.map +1 -1
- package/out/foundation/Fetcher.js +8 -13
- package/out/foundation/Fetcher.js.map +1 -1
- package/out/foundation/Injection.d.ts +87 -0
- package/out/foundation/Injection.d.ts.map +1 -0
- package/out/foundation/Injection.js +149 -0
- package/out/foundation/Injection.js.map +1 -0
- package/out/foundation/Triplet.d.ts +30 -25
- package/out/foundation/Triplet.d.ts.map +1 -1
- package/out/foundation/Triplet.js +96 -115
- package/out/foundation/Triplet.js.map +1 -1
- package/out/foundation/TripletDecorator.d.ts +11 -0
- package/out/foundation/TripletDecorator.d.ts.map +1 -1
- package/out/foundation/TripletDecorator.js +25 -8
- package/out/foundation/TripletDecorator.js.map +1 -1
- package/out/foundation/component_api/UniHtml.d.ts +5 -1
- package/out/foundation/component_api/UniHtml.d.ts.map +1 -1
- package/out/foundation/component_api/UniHtml.js +23 -5
- package/out/foundation/component_api/UniHtml.js.map +1 -1
- package/out/foundation/worker/ServiceWorker.d.ts +48 -17
- package/out/foundation/worker/ServiceWorker.d.ts.map +1 -1
- package/out/foundation/worker/ServiceWorker.js +186 -119
- package/out/foundation/worker/ServiceWorker.js.map +1 -1
- package/out/index.d.ts +4 -3
- package/out/index.d.ts.map +1 -1
- package/out/index.js +3 -2
- package/out/index.js.map +1 -1
- package/package.json +1 -1
- package/src/foundation/Fetcher.ts +9 -14
- package/src/foundation/Injection.ts +183 -0
- package/src/foundation/Triplet.ts +114 -141
- package/src/foundation/TripletDecorator.ts +32 -8
- package/src/foundation/component_api/UniHtml.ts +26 -5
- package/src/foundation/worker/ServiceWorker.ts +203 -129
- package/src/foundation/worker/serviceworker.js +191 -0
- package/src/index.ts +8 -5
- package/out/foundation/worker/serviceworker.d.ts +0 -1
- package/out/foundation/worker/serviceworker.d.ts.map +0 -1
- package/out/foundation/worker/serviceworker.js +0 -2
- package/out/foundation/worker/serviceworker.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnyConstructor, UniHtml } from "../index.js";
|
|
2
|
-
import Triplet, {
|
|
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
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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 (
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
static
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
+
});
|