@reidelsaltres/pureper 0.2.14 → 0.2.17
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/Theme.d.ts.map +1 -1
- package/out/foundation/Theme.js +16 -1
- package/out/foundation/Theme.js.map +1 -1
- 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/Component.d.ts.map +1 -1
- package/out/foundation/component_api/Component.js +1 -0
- package/out/foundation/component_api/Component.js.map +1 -1
- package/out/foundation/component_api/UniHtml.d.ts +6 -1
- package/out/foundation/component_api/UniHtml.d.ts.map +1 -1
- package/out/foundation/component_api/UniHtml.js +32 -7
- package/out/foundation/component_api/UniHtml.js.map +1 -1
- package/out/foundation/engine/Expression.d.ts +2 -0
- package/out/foundation/engine/Expression.d.ts.map +1 -1
- package/out/foundation/engine/Expression.js +26 -11
- package/out/foundation/engine/Expression.js.map +1 -1
- package/out/foundation/engine/StylePreprocessor.js +2 -1
- package/out/foundation/engine/StylePreprocessor.js.map +1 -1
- package/out/foundation/engine/TemplateEngine.d.ts +3 -1
- package/out/foundation/engine/TemplateEngine.d.ts.map +1 -1
- package/out/foundation/engine/TemplateEngine.js +44 -15
- package/out/foundation/engine/TemplateEngine.js.map +1 -1
- package/out/foundation/worker/Router.d.ts.map +1 -1
- package/out/foundation/worker/Router.js +6 -0
- package/out/foundation/worker/Router.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 +3 -2
- package/out/index.d.ts.map +1 -1
- package/out/index.js +2 -1
- 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/Theme.ts +17 -3
- package/src/foundation/Triplet.ts +114 -141
- package/src/foundation/TripletDecorator.ts +32 -8
- package/src/foundation/component_api/Component.ts +1 -0
- package/src/foundation/component_api/UniHtml.ts +35 -7
- package/src/foundation/engine/Expression.ts +34 -20
- package/src/foundation/engine/StylePreprocessor.ts +1 -1
- package/src/foundation/engine/TemplateEngine.ts +42 -15
- package/src/foundation/worker/Router.ts +10 -3
- package/src/foundation/worker/ServiceWorker.ts +203 -129
- package/src/foundation/worker/serviceworker.js +191 -0
- package/src/index.ts +7 -4
- 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
|
@@ -19,6 +19,9 @@ export default class Expression {
|
|
|
19
19
|
private readonly isAsync: boolean;
|
|
20
20
|
private readonly hasReturn: boolean;
|
|
21
21
|
|
|
22
|
+
private static fnCache = new Map<string, Function>();
|
|
23
|
+
private static readonly FN_CACHE_MAX = 500;
|
|
24
|
+
|
|
22
25
|
constructor(code: string) {
|
|
23
26
|
this.code = code.trim();
|
|
24
27
|
this.isAsync = this.detectAsync(this.code);
|
|
@@ -79,7 +82,7 @@ export default class Expression {
|
|
|
79
82
|
const identifiers: string[] = [];
|
|
80
83
|
// Regex для идентификаторов (не после точки)
|
|
81
84
|
const regex = /(?<![.\w])([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
|
82
|
-
|
|
85
|
+
|
|
83
86
|
// Список встроенных глобальных объектов, которые нужно игнорировать
|
|
84
87
|
const builtins = new Set([
|
|
85
88
|
'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
|
|
@@ -110,7 +113,7 @@ export default class Expression {
|
|
|
110
113
|
public transformCode(scope: Scope): string {
|
|
111
114
|
const context = scope.getVariables();
|
|
112
115
|
let transformedCode = this.code;
|
|
113
|
-
|
|
116
|
+
|
|
114
117
|
// Находим Observable переменные
|
|
115
118
|
const observableVars = new Set<string>();
|
|
116
119
|
for (const [key, value] of Object.entries(context)) {
|
|
@@ -178,7 +181,7 @@ export default class Expression {
|
|
|
178
181
|
// Handle reserved keywords used as variable names (e.g., "super")
|
|
179
182
|
const reservedKeywords = ['super', 'this', 'arguments'];
|
|
180
183
|
const keyMapping: Record<string, string> = {};
|
|
181
|
-
|
|
184
|
+
|
|
182
185
|
const keys = Object.keys(context).map(key => {
|
|
183
186
|
if (reservedKeywords.includes(key)) {
|
|
184
187
|
const safeKey = `__${key}__`;
|
|
@@ -188,9 +191,9 @@ export default class Expression {
|
|
|
188
191
|
return key;
|
|
189
192
|
});
|
|
190
193
|
const values = Object.values(context);
|
|
191
|
-
|
|
194
|
+
|
|
192
195
|
let codeToExecute = codeOverride ?? this.code;
|
|
193
|
-
|
|
196
|
+
|
|
194
197
|
// Replace reserved keywords in code with safe alternatives
|
|
195
198
|
for (const [original, safe] of Object.entries(keyMapping)) {
|
|
196
199
|
const regex = new RegExp(`\\b${original}\\b`, 'g');
|
|
@@ -208,19 +211,30 @@ export default class Expression {
|
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
} catch (syntaxError) {
|
|
215
|
-
// If implicit return fails, try without it (for statements)
|
|
216
|
-
if (!this.hasReturn) {
|
|
214
|
+
const cacheKey = keys.join('\0') + '\0' + functionBody;
|
|
215
|
+
let fn = Expression.fnCache.get(cacheKey);
|
|
216
|
+
if (!fn) {
|
|
217
217
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
fn = new Function(...keys, functionBody);
|
|
219
|
+
} catch (syntaxError) {
|
|
220
|
+
if (!this.hasReturn) {
|
|
221
|
+
try {
|
|
222
|
+
fn = new Function(...keys, codeToExecute);
|
|
223
|
+
} catch {
|
|
224
|
+
throw syntaxError;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
throw syntaxError;
|
|
228
|
+
}
|
|
222
229
|
}
|
|
230
|
+
if (Expression.fnCache.size >= Expression.FN_CACHE_MAX) {
|
|
231
|
+
const firstKey = Expression.fnCache.keys().next().value;
|
|
232
|
+
Expression.fnCache.delete(firstKey);
|
|
233
|
+
}
|
|
234
|
+
Expression.fnCache.set(cacheKey, fn);
|
|
223
235
|
}
|
|
236
|
+
return fn.apply(null, values);
|
|
237
|
+
} catch (syntaxError) {
|
|
224
238
|
throw syntaxError;
|
|
225
239
|
}
|
|
226
240
|
}
|
|
@@ -232,7 +246,7 @@ export default class Expression {
|
|
|
232
246
|
// Handle reserved keywords used as variable names (e.g., "super")
|
|
233
247
|
const reservedKeywords = ['super', 'this', 'arguments'];
|
|
234
248
|
const keyMapping: Record<string, string> = {};
|
|
235
|
-
|
|
249
|
+
|
|
236
250
|
const keys = Object.keys(context).map(key => {
|
|
237
251
|
if (reservedKeywords.includes(key)) {
|
|
238
252
|
const safeKey = `__${key}__`;
|
|
@@ -242,9 +256,9 @@ export default class Expression {
|
|
|
242
256
|
return key;
|
|
243
257
|
});
|
|
244
258
|
const values = Object.values(context);
|
|
245
|
-
|
|
259
|
+
|
|
246
260
|
let codeToExecute = codeOverride ?? this.code;
|
|
247
|
-
|
|
261
|
+
|
|
248
262
|
// Replace reserved keywords in code with safe alternatives
|
|
249
263
|
for (const [original, safe] of Object.entries(keyMapping)) {
|
|
250
264
|
const regex = new RegExp(`\\b${original}\\b`, 'g');
|
|
@@ -261,13 +275,13 @@ export default class Expression {
|
|
|
261
275
|
|
|
262
276
|
try {
|
|
263
277
|
// Create async function
|
|
264
|
-
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
278
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
265
279
|
const fn = new AsyncFunction(...keys, functionBody);
|
|
266
280
|
return await fn.apply(null, values);
|
|
267
281
|
} catch (syntaxError) {
|
|
268
282
|
if (!this.hasReturn) {
|
|
269
283
|
try {
|
|
270
|
-
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
284
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
271
285
|
const fn = new AsyncFunction(...keys, codeToExecute);
|
|
272
286
|
return await fn.apply(null, values);
|
|
273
287
|
} catch {
|
|
@@ -25,7 +25,7 @@ export default class TemplateEngine {
|
|
|
25
25
|
context!.data!.set(context!.refName, context!.element);
|
|
26
26
|
this.globalScope!.set(context!.refName, context!.element);
|
|
27
27
|
|
|
28
|
-
this.engine.
|
|
28
|
+
this.engine.addBinding(context!.element, () => {
|
|
29
29
|
context!.data!.delete(context!.refName);
|
|
30
30
|
this.globalScope!.delete(context!.refName);
|
|
31
31
|
});
|
|
@@ -46,10 +46,12 @@ export default class TemplateEngine {
|
|
|
46
46
|
const of = valueExpression.eval(data!);
|
|
47
47
|
const value = of instanceof Observable ? of.getObject() : of;
|
|
48
48
|
if (of instanceof Observable) {
|
|
49
|
-
|
|
49
|
+
const handler = (newValue: any) => {
|
|
50
50
|
this.engine.change();
|
|
51
51
|
this.doWork({ element, name: attributeName, value: newValue });
|
|
52
|
-
}
|
|
52
|
+
};
|
|
53
|
+
of.subscribe(handler);
|
|
54
|
+
this.engine.addBinding(element, () => of.unsubscribe(handler));
|
|
53
55
|
}
|
|
54
56
|
this.doWork({ element, name: attributeName, value });
|
|
55
57
|
}
|
|
@@ -82,6 +84,7 @@ export default class TemplateEngine {
|
|
|
82
84
|
};
|
|
83
85
|
|
|
84
86
|
element.addEventListener(eventName, listener);
|
|
87
|
+
this.engine.addBinding(element, () => element.removeEventListener(eventName, listener));
|
|
85
88
|
}
|
|
86
89
|
return false;
|
|
87
90
|
}
|
|
@@ -140,11 +143,13 @@ export default class TemplateEngine {
|
|
|
140
143
|
|
|
141
144
|
const value = of instanceof Observable ? of.getObject() : of;
|
|
142
145
|
if (of instanceof Observable) {
|
|
143
|
-
|
|
146
|
+
const handler = (newValue: any[]) => {
|
|
144
147
|
element.textContent = "";
|
|
145
148
|
this.engine.change();
|
|
146
149
|
this.doWork({ element, value: newValue, allowHtmlInjection: allowHtmlInjection });
|
|
147
|
-
}
|
|
150
|
+
};
|
|
151
|
+
of.subscribe(handler);
|
|
152
|
+
this.engine.addBinding(element, () => of.unsubscribe(handler));
|
|
148
153
|
}
|
|
149
154
|
this.doWork({ element, value, allowHtmlInjection: allowHtmlInjection });
|
|
150
155
|
|
|
@@ -179,10 +184,12 @@ export default class TemplateEngine {
|
|
|
179
184
|
|
|
180
185
|
const iterable = of instanceof Observable ? of.getObject() : of;
|
|
181
186
|
if (of instanceof Observable) {
|
|
182
|
-
|
|
187
|
+
const handler = (newValue: any[]) => {
|
|
183
188
|
this.engine.change();
|
|
184
189
|
this.doWork({ element, iterable: newValue, index, value, walker, shadow, data });
|
|
185
|
-
}
|
|
190
|
+
};
|
|
191
|
+
of.subscribe(handler);
|
|
192
|
+
this.engine.addBinding(element, () => of.unsubscribe(handler));
|
|
186
193
|
}
|
|
187
194
|
this.doWork({ element, iterable: iterable, index, value, walker, shadow, data });
|
|
188
195
|
}
|
|
@@ -204,6 +211,7 @@ export default class TemplateEngine {
|
|
|
204
211
|
const data = context!.data;
|
|
205
212
|
|
|
206
213
|
element.childNodes.forEach(n => this.engine.fullCleanup(n));
|
|
214
|
+
while (element.firstChild) element.removeChild(element.firstChild);
|
|
207
215
|
|
|
208
216
|
const lenght = typeof iterable === "number" ? iterable : iterable.length;
|
|
209
217
|
|
|
@@ -258,10 +266,12 @@ export default class TemplateEngine {
|
|
|
258
266
|
const condition = new Expression(conditionAtt).eval(data!);
|
|
259
267
|
|
|
260
268
|
if (condition instanceof Observable) {
|
|
261
|
-
|
|
269
|
+
const handler = (newValue: boolean) => {
|
|
262
270
|
this.engine.change();
|
|
263
271
|
this.doWork({ walker, data, allParts: allParts });
|
|
264
|
-
}
|
|
272
|
+
};
|
|
273
|
+
condition.subscribe(handler);
|
|
274
|
+
this.engine.addBinding(part.element, () => condition.unsubscribe(handler));
|
|
265
275
|
}
|
|
266
276
|
}
|
|
267
277
|
|
|
@@ -278,9 +288,8 @@ export default class TemplateEngine {
|
|
|
278
288
|
const { walker, data, allParts } = context!;
|
|
279
289
|
|
|
280
290
|
allParts.forEach(part => {
|
|
281
|
-
this.engine.fullCleanup(
|
|
282
|
-
|
|
283
|
-
this.engine.fullCleanup(n));*/
|
|
291
|
+
part.element.childNodes.forEach(n => this.engine.fullCleanup(n));
|
|
292
|
+
while (part.element.firstChild) part.element.removeChild(part.element.firstChild);
|
|
284
293
|
});
|
|
285
294
|
|
|
286
295
|
for (const part of allParts) {
|
|
@@ -316,9 +325,24 @@ export default class TemplateEngine {
|
|
|
316
325
|
this.if_component
|
|
317
326
|
];
|
|
318
327
|
|
|
319
|
-
public readonly bindings: Map<
|
|
328
|
+
public readonly bindings: Map<Node, (() => void)[]> = new Map();
|
|
320
329
|
private readonly onChangeCallbacks: (() => void)[] = [];
|
|
321
330
|
public processLogs: string[] = [];
|
|
331
|
+
public addBinding(node: Node, cleanup: () => void): void {
|
|
332
|
+
const existing = this.bindings.get(node);
|
|
333
|
+
if (existing) {
|
|
334
|
+
existing.push(cleanup);
|
|
335
|
+
} else {
|
|
336
|
+
this.bindings.set(node, [cleanup]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
public dispose(): void {
|
|
340
|
+
for (const [, cleanups] of this.bindings) {
|
|
341
|
+
cleanups.forEach(cleanup => cleanup());
|
|
342
|
+
}
|
|
343
|
+
this.bindings.clear();
|
|
344
|
+
this.onChangeCallbacks.length = 0;
|
|
345
|
+
}
|
|
322
346
|
public static fullProcess(root: Node, scope: Scope): string[] {
|
|
323
347
|
const engine = new TemplateEngine();
|
|
324
348
|
engine.fullProcess(root, scope);
|
|
@@ -379,8 +403,11 @@ export default class TemplateEngine {
|
|
|
379
403
|
let i = 0;
|
|
380
404
|
walker.onEnterNode((node: Node, data?: Scope) => {
|
|
381
405
|
const element = node as Element;
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
406
|
+
const bindings = this.bindings.get(element);
|
|
407
|
+
if (bindings) {
|
|
408
|
+
bindings.forEach(b => b());
|
|
409
|
+
this.bindings.delete(element);
|
|
410
|
+
}
|
|
384
411
|
|
|
385
412
|
this.processLogs.push((" ").repeat(i) + "layer " + i + " Cleaning up Starting: " + node.nodeName);
|
|
386
413
|
i++;
|
|
@@ -50,14 +50,21 @@ export abstract class Router {
|
|
|
50
50
|
window.location.replace(url.href);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
public static tryRouteTo(url: URL, pushState: boolean = true) {
|
|
55
55
|
const urlH = new URL(Fetcher.resolveUrl(url.href));
|
|
56
56
|
try {
|
|
57
57
|
const found: Route = this.tryFindRoute(urlH);
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
let pageContainer = document.getElementById('page');
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
if (this.currentPage) {
|
|
62
|
+
this.currentPage.dispose();
|
|
63
|
+
}
|
|
64
|
+
while (pageContainer.firstChild) {
|
|
65
|
+
pageContainer.removeChild(pageContainer.firstChild);
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
const page: UniHtml = this.createPage(found, urlH.searchParams);
|
|
62
69
|
this.currentPage = page;
|
|
63
70
|
|
|
@@ -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
|
}
|