@reidelsaltres/pureper 0.2.33 → 0.2.34
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/CacheManager.d.ts +20 -0
- package/out/foundation/CacheManager.d.ts.map +1 -0
- package/out/foundation/CacheManager.js +129 -0
- package/out/foundation/CacheManager.js.map +1 -0
- package/out/foundation/Injection.d.ts +6 -0
- package/out/foundation/Injection.d.ts.map +1 -1
- package/out/foundation/Injection.js +23 -0
- package/out/foundation/Injection.js.map +1 -1
- package/out/foundation/Module.d.ts +41 -24
- package/out/foundation/Module.d.ts.map +1 -1
- package/out/foundation/Module.js +253 -152
- package/out/foundation/Module.js.map +1 -1
- package/out/foundation/Theme.d.ts +2 -0
- package/out/foundation/Theme.d.ts.map +1 -1
- package/out/foundation/Theme.js +9 -1
- package/out/foundation/Theme.js.map +1 -1
- package/out/foundation/Triplet.d.ts +10 -0
- package/out/foundation/Triplet.d.ts.map +1 -1
- package/out/foundation/Triplet.js +17 -0
- package/out/foundation/Triplet.js.map +1 -1
- package/out/foundation/TripletDecorator.d.ts.map +1 -1
- package/out/foundation/TripletDecorator.js +28 -1
- package/out/foundation/TripletDecorator.js.map +1 -1
- package/out/foundation/engine/Expression.d.ts.map +1 -1
- package/out/foundation/engine/Expression.js +11 -11
- package/out/foundation/engine/Expression.js.map +1 -1
- package/out/foundation/engine/TemplateEngine.d.ts.map +1 -1
- package/out/foundation/engine/TemplateEngine.js +6 -2
- package/out/foundation/engine/TemplateEngine.js.map +1 -1
- package/out/foundation/worker/ServiceWorker.d.ts +2 -25
- package/out/foundation/worker/ServiceWorker.d.ts.map +1 -1
- package/out/foundation/worker/ServiceWorker.js +2 -94
- 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/CacheManager.ts +151 -0
- package/src/foundation/Injection.ts +24 -0
- package/src/foundation/Module.ts +282 -174
- package/src/foundation/Theme.ts +10 -1
- package/src/foundation/Triplet.ts +21 -0
- package/src/foundation/TripletDecorator.ts +19 -1
- package/src/foundation/engine/Expression.ts +11 -12
- package/src/foundation/engine/TemplateEngine.ts +6 -3
- package/src/foundation/worker/ServiceWorker.ts +2 -101
- package/src/index.ts +4 -3
package/src/foundation/Module.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import Observable from "./api/Observer.js";
|
|
2
|
-
import {
|
|
2
|
+
import { Placeholder } from "./Injection.js";
|
|
3
|
+
import { REGISTRY, RegistryCapture } from "./Triplet.js";
|
|
4
|
+
import CacheManager, { DownloadProgress } from "./CacheManager.js";
|
|
5
|
+
|
|
6
|
+
export type SubModuleStruct = {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
inbuilt?: boolean;
|
|
10
|
+
resources?: string[];
|
|
11
|
+
};
|
|
3
12
|
|
|
4
13
|
export type ModuleStruct = {
|
|
5
14
|
name: string;
|
|
@@ -8,51 +17,50 @@ export type ModuleStruct = {
|
|
|
8
17
|
core?: boolean;
|
|
9
18
|
enabled?: boolean;
|
|
10
19
|
resources?: string[];
|
|
20
|
+
subModules?: SubModuleStruct[];
|
|
11
21
|
};
|
|
12
22
|
|
|
13
|
-
export
|
|
14
|
-
/** Total number of resources */
|
|
15
|
-
totalFiles: number;
|
|
16
|
-
/** Number of resources downloaded so far */
|
|
17
|
-
downloadedFiles: number;
|
|
18
|
-
/** Current file being downloaded */
|
|
19
|
-
currentFile: string;
|
|
20
|
-
/** Total bytes across all resources (estimated) */
|
|
21
|
-
totalBytes: number;
|
|
22
|
-
/** Bytes downloaded so far */
|
|
23
|
-
downloadedBytes: number;
|
|
24
|
-
/** Download speed in bytes/sec */
|
|
25
|
-
speed: number;
|
|
26
|
-
/** Whether download is in progress */
|
|
27
|
-
active: boolean;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export default class Module {
|
|
23
|
+
export default class Module extends Observable<boolean> {
|
|
31
24
|
public readonly name: string;
|
|
32
25
|
public readonly description?: string;
|
|
33
26
|
public readonly icon?: string;
|
|
34
27
|
public readonly core: boolean;
|
|
35
|
-
public readonly enabled: Observable<boolean>;
|
|
36
28
|
public readonly downloaded: Observable<boolean>;
|
|
37
29
|
public readonly resources: string[];
|
|
38
30
|
public readonly totalSize: Observable<number>;
|
|
39
|
-
public readonly downloadProgress: Observable<
|
|
31
|
+
public readonly downloadProgress: Observable<DownloadProgress>;
|
|
32
|
+
public readonly downloadError: Observable<string>;
|
|
40
33
|
|
|
41
34
|
private _registrations: (() => Promise<void>)[] = [];
|
|
35
|
+
private _placeholderNames: string[] = [];
|
|
36
|
+
private _initialized: boolean = false;
|
|
37
|
+
private _subModules: SubModule[] = [];
|
|
38
|
+
private static _claimedPlaceholders = new Set<string>();
|
|
39
|
+
|
|
40
|
+
public get enabled(): Observable<boolean> {
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
42
43
|
|
|
43
44
|
public constructor(struct: ModuleStruct) {
|
|
45
|
+
super(struct.core ? true : (struct.enabled ?? false));
|
|
44
46
|
this.name = struct.name;
|
|
45
47
|
this.description = struct.description;
|
|
46
48
|
this.icon = struct.icon;
|
|
47
49
|
this.core = struct.core ?? false;
|
|
48
|
-
this.
|
|
49
|
-
this.downloaded = new Observable<boolean>(this.core ? true : false);
|
|
50
|
+
this.downloaded = new Observable<boolean>(false);
|
|
50
51
|
this.resources = struct.resources ?? [];
|
|
51
52
|
this.totalSize = new Observable<number>(0);
|
|
52
|
-
this.downloadProgress = new Observable<
|
|
53
|
-
totalFiles: 0,
|
|
53
|
+
this.downloadProgress = new Observable<DownloadProgress>({
|
|
54
|
+
totalFiles: 0, completedFiles: 0, currentFile: '',
|
|
54
55
|
totalBytes: 0, downloadedBytes: 0, speed: 0, active: false
|
|
55
56
|
});
|
|
57
|
+
this.downloadError = new Observable<string>('');
|
|
58
|
+
|
|
59
|
+
if (struct.subModules) {
|
|
60
|
+
for (const sub of struct.subModules) {
|
|
61
|
+
this.addSubModule(sub);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
public addRegistration(fn: () => Promise<void>): void {
|
|
@@ -73,12 +81,60 @@ export default class Module {
|
|
|
73
81
|
public captureRegistrations(registry: (() => Promise<void>)[]): void {
|
|
74
82
|
const captured = registry.splice(0, registry.length);
|
|
75
83
|
this._registrations.push(...captured);
|
|
76
|
-
|
|
84
|
+
|
|
85
|
+
// Discover placeholder names created by this module's imports
|
|
86
|
+
for (const name of Placeholder.getAllNames()) {
|
|
87
|
+
if (!Module._claimedPlaceholders.has(name)) {
|
|
88
|
+
Module._claimedPlaceholders.add(name);
|
|
89
|
+
this._placeholderNames.push(name);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`[Module]: "${this.name}" captured ${captured.length} registration(s), ${this._placeholderNames.length} placeholder(s)`);
|
|
94
|
+
|
|
95
|
+
const capturedResources = RegistryCapture.drain();
|
|
96
|
+
if (capturedResources.length > 0) {
|
|
97
|
+
this.resources.push(...capturedResources);
|
|
98
|
+
console.log(`[Module]: "${this.name}" auto-captured ${capturedResources.length} resource path(s)`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public addPlaceholder(name: string): void {
|
|
103
|
+
this._placeholderNames.push(name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public getPlaceholderNames(): ReadonlyArray<string> {
|
|
107
|
+
return this._placeholderNames;
|
|
77
108
|
}
|
|
78
109
|
|
|
79
|
-
public
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
public markInitialized(): void {
|
|
111
|
+
this._initialized = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public async enable(): Promise<void> {
|
|
115
|
+
if (this.getObject() === true) return;
|
|
116
|
+
this.setObject(true);
|
|
117
|
+
|
|
118
|
+
if (!this._initialized) {
|
|
119
|
+
this._initialized = true;
|
|
120
|
+
// Snapshot existing placeholders before running registrations
|
|
121
|
+
const before = new Set(Placeholder.getAllNames());
|
|
122
|
+
for (const reg of this._registrations) {
|
|
123
|
+
await reg();
|
|
124
|
+
}
|
|
125
|
+
// Discover which placeholders were added by this module
|
|
126
|
+
for (const name of Placeholder.getAllNames()) {
|
|
127
|
+
if (!before.has(name)) {
|
|
128
|
+
this._placeholderNames.push(name);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// Re-activate previously registered placeholders
|
|
133
|
+
for (const name of this._placeholderNames) {
|
|
134
|
+
Placeholder.activate(name);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
82
138
|
console.log(`[Module]: "${this.name}" enabled`);
|
|
83
139
|
}
|
|
84
140
|
|
|
@@ -86,13 +142,19 @@ export default class Module {
|
|
|
86
142
|
if (this.core) {
|
|
87
143
|
throw new Error(`[Module]: Cannot disable core module "${this.name}"`);
|
|
88
144
|
}
|
|
89
|
-
if (this.
|
|
90
|
-
this.
|
|
145
|
+
if (this.getObject() === false) return;
|
|
146
|
+
this.setObject(false);
|
|
147
|
+
|
|
148
|
+
// Deactivate placeholders
|
|
149
|
+
for (const name of this._placeholderNames) {
|
|
150
|
+
Placeholder.deactivate(name);
|
|
151
|
+
}
|
|
152
|
+
|
|
91
153
|
console.log(`[Module]: "${this.name}" disabled`);
|
|
92
154
|
}
|
|
93
155
|
|
|
94
156
|
public isActive(): boolean {
|
|
95
|
-
return this.
|
|
157
|
+
return this.getObject() === true;
|
|
96
158
|
}
|
|
97
159
|
|
|
98
160
|
/** True if enabled but not downloaded — works only in the current session. */
|
|
@@ -100,172 +162,161 @@ export default class Module {
|
|
|
100
162
|
return this.isActive() && !this.downloaded.getObject();
|
|
101
163
|
}
|
|
102
164
|
|
|
103
|
-
/**
|
|
104
|
-
* Download all module resources with progress tracking.
|
|
105
|
-
* Uses fetch to download each resource and tracks progress.
|
|
106
|
-
*/
|
|
107
165
|
public async download(): Promise<void> {
|
|
108
166
|
if (this.downloaded.getObject()) return;
|
|
109
|
-
|
|
110
|
-
|
|
167
|
+
this.downloadError.setObject('');
|
|
168
|
+
|
|
169
|
+
const hasInbuiltSubs = this._subModules.some(s => s.inbuilt && !s.downloaded.getObject() && s.resources.length > 0);
|
|
170
|
+
if (this.resources.length === 0 && !hasInbuiltSubs) {
|
|
111
171
|
this.downloaded.setObject(true);
|
|
112
172
|
console.log(`[Module]: "${this.name}" downloaded (no resources)`);
|
|
113
173
|
return;
|
|
114
174
|
}
|
|
115
175
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
let lastBytes = 0;
|
|
129
|
-
let speed = 0;
|
|
130
|
-
|
|
131
|
-
// First, try to estimate total size with HEAD requests
|
|
132
|
-
const headPromises = this.resources.map(async (url) => {
|
|
133
|
-
try {
|
|
134
|
-
const resolved = resolveUrl(url);
|
|
135
|
-
const resp = await fetch(resolved, { method: 'HEAD' });
|
|
136
|
-
const cl = resp.headers.get('content-length');
|
|
137
|
-
return cl ? parseInt(cl, 10) : 0;
|
|
138
|
-
} catch {
|
|
139
|
-
return 0;
|
|
176
|
+
try {
|
|
177
|
+
// Download module's own resources
|
|
178
|
+
let totalBytes = await CacheManager.download(this.resources, this.downloadProgress);
|
|
179
|
+
|
|
180
|
+
// Also download all inbuilt sub-modules
|
|
181
|
+
for (const sub of this._subModules) {
|
|
182
|
+
if (sub.inbuilt && !sub.downloaded.getObject()) {
|
|
183
|
+
const subBytes = await CacheManager.download(sub.resources, this.downloadProgress);
|
|
184
|
+
sub.totalSize.setObject(subBytes);
|
|
185
|
+
sub.downloaded.setObject(true);
|
|
186
|
+
totalBytes += subBytes;
|
|
187
|
+
}
|
|
140
188
|
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
const sizes = await Promise.all(headPromises);
|
|
144
|
-
totalBytes = sizes.reduce((a, b) => a + b, 0);
|
|
145
|
-
this.totalSize.setObject(totalBytes);
|
|
146
|
-
|
|
147
|
-
this.downloadProgress.setObject({
|
|
148
|
-
totalFiles, downloadedFiles: 0, currentFile: '',
|
|
149
|
-
totalBytes, downloadedBytes: 0, speed: 0, active: true
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
console.log(`[Module]: Starting download of "${this.name}" (${totalFiles} files, ~${this.formatBytes(totalBytes)})`);
|
|
153
189
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
190
|
+
this.totalSize.setObject(totalBytes);
|
|
191
|
+
this.downloaded.setObject(true);
|
|
192
|
+
// Clear ephemeral flag if re-downloaded
|
|
193
|
+
ModuleManager.clearEphemeralCore(this.name);
|
|
194
|
+
console.log(`[Module]: "${this.name}" downloaded (${this.resources.length} files, ${CacheManager.formatBytes(totalBytes)})`);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
197
|
+
this.downloadError.setObject(msg);
|
|
198
|
+
console.error(`[Module]: "${this.name}" download failed:`, msg);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
158
201
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
});
|
|
202
|
+
public formatBytes(bytes: number): string {
|
|
203
|
+
return CacheManager.formatBytes(bytes);
|
|
204
|
+
}
|
|
163
205
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
206
|
+
public async undownload(): Promise<void> {
|
|
207
|
+
this.downloadError.setObject('');
|
|
208
|
+
await CacheManager.delete(this.resources);
|
|
209
|
+
|
|
210
|
+
// Also undownload all sub-modules
|
|
211
|
+
for (const sub of this._subModules) {
|
|
212
|
+
if (sub.downloaded.getObject()) {
|
|
213
|
+
if (sub.inbuilt) {
|
|
214
|
+
// Inbuilt subs: clean up directly (bypass the guard)
|
|
215
|
+
await CacheManager.delete(sub.resources);
|
|
216
|
+
sub.downloaded.setObject(false);
|
|
217
|
+
sub.totalSize.setObject(0);
|
|
218
|
+
sub.downloadError.setObject('');
|
|
219
|
+
} else {
|
|
220
|
+
await sub.undownload();
|
|
170
221
|
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
171
224
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (reader) {
|
|
177
|
-
while (true) {
|
|
178
|
-
const { done, value } = await reader.read();
|
|
179
|
-
if (done) break;
|
|
180
|
-
chunks.push(value);
|
|
181
|
-
downloadedBytes += value.byteLength;
|
|
182
|
-
|
|
183
|
-
// Calculate speed
|
|
184
|
-
const now = performance.now();
|
|
185
|
-
const elapsed = (now - lastTime) / 1000;
|
|
186
|
-
if (elapsed >= 0.5) {
|
|
187
|
-
speed = (downloadedBytes - lastBytes) / elapsed;
|
|
188
|
-
lastTime = now;
|
|
189
|
-
lastBytes = downloadedBytes;
|
|
190
|
-
}
|
|
225
|
+
this.downloaded.setObject(false);
|
|
226
|
+
this.totalSize.setObject(0);
|
|
227
|
+
console.log(`[Module]: "${this.name}" removed from downloads`);
|
|
228
|
+
}
|
|
191
229
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
230
|
+
public addSubModule(struct: SubModuleStruct): SubModule {
|
|
231
|
+
const sub = new SubModule(struct, this);
|
|
232
|
+
this._subModules.push(sub);
|
|
233
|
+
return sub;
|
|
234
|
+
}
|
|
198
235
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const blob = new Blob(chunks);
|
|
204
|
-
const cacheResponse = new Response(blob, {
|
|
205
|
-
headers: response.headers
|
|
206
|
-
});
|
|
207
|
-
await cache.put(resolved, cacheResponse);
|
|
208
|
-
} catch (e) {
|
|
209
|
-
console.warn(`[Module]: Failed to cache ${url}`, e);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
236
|
+
public getSubModules(): ReadonlyArray<SubModule> {
|
|
237
|
+
return this._subModules;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
212
240
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
241
|
+
export class SubModule {
|
|
242
|
+
public readonly name: string;
|
|
243
|
+
public readonly description?: string;
|
|
244
|
+
public readonly inbuilt: boolean;
|
|
245
|
+
public readonly resources: string[];
|
|
246
|
+
public readonly downloaded: Observable<boolean>;
|
|
247
|
+
public readonly parent: Module;
|
|
248
|
+
public readonly totalSize: Observable<number>;
|
|
249
|
+
public readonly downloadProgress: Observable<DownloadProgress>;
|
|
250
|
+
public readonly downloadError: Observable<string>;
|
|
219
251
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
252
|
+
constructor(struct: SubModuleStruct, parent: Module) {
|
|
253
|
+
this.name = struct.name;
|
|
254
|
+
this.description = struct.description;
|
|
255
|
+
this.inbuilt = struct.inbuilt ?? true;
|
|
256
|
+
this.resources = struct.resources ?? [];
|
|
257
|
+
this.downloaded = new Observable<boolean>(false);
|
|
258
|
+
this.parent = parent;
|
|
259
|
+
this.totalSize = new Observable<number>(0);
|
|
260
|
+
this.downloadProgress = new Observable<DownloadProgress>({
|
|
261
|
+
totalFiles: 0, completedFiles: 0, currentFile: '',
|
|
262
|
+
totalBytes: 0, downloadedBytes: 0, speed: 0, active: false
|
|
223
263
|
});
|
|
224
|
-
|
|
225
|
-
this.downloaded.setObject(true);
|
|
226
|
-
console.log(`[Module]: "${this.name}" downloaded (${downloadedFiles}/${totalFiles} files, ${this.formatBytes(downloadedBytes)})`);
|
|
264
|
+
this.downloadError = new Observable<string>('');
|
|
227
265
|
}
|
|
228
266
|
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
267
|
+
private assertParentActive(): void {
|
|
268
|
+
if (!this.parent.isActive()) {
|
|
269
|
+
throw new Error(`[SubModule]: Cannot access "${this.name}" — parent module "${this.parent.name}" is disabled`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
private assertParentDownloaded(): void {
|
|
273
|
+
if (!this.parent.downloaded.getObject()) {
|
|
274
|
+
throw new Error(`[SubModule]: Cannot access "${this.name}" — parent module "${this.parent.name}" is not downloaded`);
|
|
275
|
+
}
|
|
235
276
|
}
|
|
236
277
|
|
|
237
|
-
public async
|
|
238
|
-
if (this.
|
|
239
|
-
throw new Error(`[
|
|
278
|
+
public async download(): Promise<void> {
|
|
279
|
+
if (this.inbuilt) {
|
|
280
|
+
throw new Error(`[SubModule]: "${this.name}" is inbuilt and cannot be downloaded separately`);
|
|
240
281
|
}
|
|
282
|
+
this.assertParentDownloaded();
|
|
283
|
+
if (this.downloaded.getObject()) return;
|
|
284
|
+
this.downloadError.setObject('');
|
|
241
285
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const cache = await caches.open('purper-modules');
|
|
246
|
-
const resolveUrl = (url: string): string => {
|
|
247
|
-
const trimmed = url.trim();
|
|
248
|
-
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return trimmed;
|
|
249
|
-
if (trimmed.startsWith("//")) return `${window.location.protocol}${trimmed}`;
|
|
250
|
-
return new URL(trimmed, document.baseURI).href;
|
|
251
|
-
};
|
|
252
|
-
for (const url of this.resources) {
|
|
253
|
-
await cache.delete(resolveUrl(url));
|
|
254
|
-
}
|
|
255
|
-
console.log(`[Module]: Cleared cache for "${this.name}"`);
|
|
256
|
-
} catch (e) {
|
|
257
|
-
console.warn(`[Module]: Failed to clear cache for "${this.name}"`, e);
|
|
258
|
-
}
|
|
286
|
+
if (this.resources.length === 0) {
|
|
287
|
+
this.downloaded.setObject(true);
|
|
288
|
+
return;
|
|
259
289
|
}
|
|
260
290
|
|
|
291
|
+
try {
|
|
292
|
+
const bytes = await CacheManager.download(this.resources, this.downloadProgress);
|
|
293
|
+
this.totalSize.setObject(bytes);
|
|
294
|
+
this.downloaded.setObject(true);
|
|
295
|
+
console.log(`[SubModule]: "${this.name}" of "${this.parent.name}" downloaded (${CacheManager.formatBytes(bytes)})`);
|
|
296
|
+
} catch (e) {
|
|
297
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
298
|
+
this.downloadError.setObject(msg);
|
|
299
|
+
console.error(`[SubModule]: "${this.name}" download failed:`, msg);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
public async undownload(): Promise<void> {
|
|
304
|
+
if (this.inbuilt) {
|
|
305
|
+
throw new Error(`[SubModule]: "${this.name}" is inbuilt and cannot be removed separately`);
|
|
306
|
+
}
|
|
307
|
+
if (!this.downloaded.getObject()) return;
|
|
308
|
+
this.downloadError.setObject('');
|
|
309
|
+
|
|
310
|
+
await CacheManager.delete(this.resources);
|
|
261
311
|
this.downloaded.setObject(false);
|
|
262
312
|
this.totalSize.setObject(0);
|
|
263
|
-
console.log(`[
|
|
313
|
+
console.log(`[SubModule]: "${this.name}" of "${this.parent.name}" removed from downloads`);
|
|
264
314
|
}
|
|
265
315
|
}
|
|
266
316
|
|
|
267
317
|
export class ModuleManager {
|
|
268
318
|
private static _modules: Map<string, Module> = new Map();
|
|
319
|
+
private static _userEphemeralCores = new Set<string>();
|
|
269
320
|
private static readonly STORAGE_KEY = "purper:modules";
|
|
270
321
|
private static readonly SESSION_KEY = "purper:modules:session";
|
|
271
322
|
|
|
@@ -306,6 +357,7 @@ export class ModuleManager {
|
|
|
306
357
|
|
|
307
358
|
for (const mod of this._modules.values()) {
|
|
308
359
|
if (mod.isActive()) {
|
|
360
|
+
mod.markInitialized();
|
|
309
361
|
console.log(`[Module]: Initializing module "${mod.name}" (${mod.getRegistrations().length} registration(s))`);
|
|
310
362
|
for (const reg of mod.getRegistrations()) {
|
|
311
363
|
promises.push(reg());
|
|
@@ -324,18 +376,50 @@ export class ModuleManager {
|
|
|
324
376
|
}
|
|
325
377
|
}
|
|
326
378
|
|
|
379
|
+
// Auto-download enabled modules that are not yet downloaded
|
|
380
|
+
// (fire-and-forget — does not block initialization)
|
|
381
|
+
this.autoDownload();
|
|
382
|
+
|
|
327
383
|
return promises;
|
|
328
384
|
}
|
|
329
385
|
|
|
386
|
+
private static async autoDownload(): Promise<void> {
|
|
387
|
+
for (const mod of this._modules.values()) {
|
|
388
|
+
if (mod.core && !mod.downloaded.getObject() && !this._userEphemeralCores.has(mod.name)) {
|
|
389
|
+
console.log(`[Module]: Auto-downloading "${mod.name}"...`);
|
|
390
|
+
mod.download().then(() => {
|
|
391
|
+
this.persistState();
|
|
392
|
+
}).catch(e => {
|
|
393
|
+
console.warn(`[Module]: Auto-download failed for "${mod.name}"`, e);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
330
399
|
static persistState(): void {
|
|
331
|
-
const localState: Record<string, { enabled: boolean, downloaded: boolean }> = {};
|
|
400
|
+
const localState: Record<string, { enabled: boolean, downloaded: boolean, size: number, subModules?: Record<string, { downloaded: boolean, size: number }> }> = {};
|
|
332
401
|
const sessionState: Record<string, { enabled: boolean }> = {};
|
|
333
402
|
|
|
334
403
|
for (const mod of this._modules.values()) {
|
|
335
|
-
|
|
404
|
+
const subs = mod.getSubModules();
|
|
405
|
+
let subModules: Record<string, { downloaded: boolean, size: number }> | undefined;
|
|
406
|
+
if (subs.length > 0) {
|
|
407
|
+
subModules = {};
|
|
408
|
+
for (const sub of subs) {
|
|
409
|
+
subModules[sub.name] = {
|
|
410
|
+
downloaded: sub.downloaded.getObject() === true,
|
|
411
|
+
size: sub.totalSize.getObject() ?? 0
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
336
415
|
|
|
337
|
-
if (mod.downloaded.getObject()) {
|
|
338
|
-
localState[mod.name] = {
|
|
416
|
+
if (mod.downloaded.getObject() || mod.core) {
|
|
417
|
+
localState[mod.name] = {
|
|
418
|
+
enabled: mod.isActive(),
|
|
419
|
+
downloaded: mod.downloaded.getObject() === true,
|
|
420
|
+
size: mod.totalSize.getObject() ?? 0,
|
|
421
|
+
...(subModules ? { subModules } : {})
|
|
422
|
+
};
|
|
339
423
|
} else if (mod.isActive()) {
|
|
340
424
|
sessionState[mod.name] = { enabled: true };
|
|
341
425
|
}
|
|
@@ -350,25 +434,49 @@ export class ModuleManager {
|
|
|
350
434
|
}
|
|
351
435
|
}
|
|
352
436
|
|
|
437
|
+
static clearEphemeralCore(name: string): void {
|
|
438
|
+
this._userEphemeralCores.delete(name);
|
|
439
|
+
}
|
|
440
|
+
|
|
353
441
|
static restoreState(): void {
|
|
354
442
|
// Restore from localStorage (downloaded modules)
|
|
355
443
|
try {
|
|
356
444
|
const raw = localStorage.getItem(this.STORAGE_KEY);
|
|
357
445
|
if (raw) {
|
|
358
|
-
const state: Record<string, { enabled: boolean, downloaded: boolean } | boolean> = JSON.parse(raw);
|
|
446
|
+
const state: Record<string, { enabled: boolean, downloaded: boolean, size?: number, subModules?: Record<string, { enabled?: boolean, downloaded: boolean, size?: number }> } | boolean> = JSON.parse(raw);
|
|
359
447
|
for (const [name, data] of Object.entries(state)) {
|
|
360
448
|
const mod = this._modules.get(name);
|
|
361
|
-
if (mod
|
|
449
|
+
if (mod) {
|
|
362
450
|
if (typeof data === 'boolean') {
|
|
363
451
|
// Old format: value is just a boolean for enabled state
|
|
364
|
-
mod.
|
|
452
|
+
if (!mod.core) mod.setObject(data);
|
|
365
453
|
console.log(`[Module]: Migrated old format "${name}" → enabled=${data}`);
|
|
366
454
|
continue;
|
|
367
455
|
}
|
|
368
456
|
if (data.downloaded) {
|
|
369
457
|
mod.downloaded.setObject(true);
|
|
370
458
|
}
|
|
371
|
-
|
|
459
|
+
if (data.size) {
|
|
460
|
+
mod.totalSize.setObject(data.size);
|
|
461
|
+
}
|
|
462
|
+
if (!mod.core) {
|
|
463
|
+
mod.setObject(data.enabled);
|
|
464
|
+
}
|
|
465
|
+
// Track core modules explicitly made ephemeral by user
|
|
466
|
+
if (mod.core && !data.downloaded) {
|
|
467
|
+
this._userEphemeralCores.add(name);
|
|
468
|
+
}
|
|
469
|
+
if (data.subModules) {
|
|
470
|
+
for (const sub of mod.getSubModules()) {
|
|
471
|
+
const subData = data.subModules[sub.name];
|
|
472
|
+
if (subData) {
|
|
473
|
+
sub.downloaded.setObject(subData.downloaded);
|
|
474
|
+
if (subData.size) {
|
|
475
|
+
sub.totalSize.setObject(subData.size);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
372
480
|
console.log(`[Module]: Restored "${name}" → downloaded=${data.downloaded}, enabled=${data.enabled}`);
|
|
373
481
|
}
|
|
374
482
|
}
|
|
@@ -385,7 +493,7 @@ export class ModuleManager {
|
|
|
385
493
|
for (const [name, data] of Object.entries(state)) {
|
|
386
494
|
const mod = this._modules.get(name);
|
|
387
495
|
if (mod && !mod.core && !mod.downloaded.getObject()) {
|
|
388
|
-
mod.
|
|
496
|
+
mod.setObject(data.enabled);
|
|
389
497
|
console.log(`[Module]: Restored ephemeral "${name}" → enabled=${data.enabled}`);
|
|
390
498
|
}
|
|
391
499
|
}
|
package/src/foundation/Theme.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Fetcher from "./Fetcher.js";
|
|
2
2
|
import Observable from "./api/Observer.js";
|
|
3
3
|
|
|
4
|
+
export const DEFAULT_THEME = "Blazor";
|
|
4
5
|
export let ACTIVE_THEME_KEY = "Empty";
|
|
5
6
|
let activeThemeSheet: CSSStyleSheet | null = null;
|
|
6
7
|
let _internalThemeSwitch = false;
|
|
@@ -20,7 +21,7 @@ export async function init() {
|
|
|
20
21
|
if (ACTIVE_THEME_KEY) {
|
|
21
22
|
await setTheme(ACTIVE_THEME_KEY);
|
|
22
23
|
} else {
|
|
23
|
-
await setTheme(
|
|
24
|
+
await setTheme(DEFAULT_THEME);
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
export async function setTheme(name: string) {
|
|
@@ -168,3 +169,11 @@ export async function deactivateAppTheme(): Promise<void> {
|
|
|
168
169
|
console.info(`[Theme]: AppTheme "${current.name}" deactivated`);
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
export async function resetToDefault(): Promise<void> {
|
|
173
|
+
await deactivateAppTheme();
|
|
174
|
+
await setTheme(DEFAULT_THEME);
|
|
175
|
+
ACTIVE_THEME_KEY = DEFAULT_THEME;
|
|
176
|
+
localStorage.setItem("theme", DEFAULT_THEME);
|
|
177
|
+
console.info(`[Theme]: Reset to default theme "${DEFAULT_THEME}"`);
|
|
178
|
+
}
|
|
179
|
+
|
|
@@ -10,6 +10,27 @@ import { Implementation, ImplementationStruct, Placeholder } from "./Injection.j
|
|
|
10
10
|
|
|
11
11
|
export const REGISTRY: (() => Promise<void>)[] = [];
|
|
12
12
|
|
|
13
|
+
export class RegistryCapture {
|
|
14
|
+
private static _unclaimed: string[] = [];
|
|
15
|
+
private static _classResources = new Map<Function, string[]>();
|
|
16
|
+
|
|
17
|
+
/** Called by decorators to record which file paths a class uses. */
|
|
18
|
+
static capture(cls: Function, paths: string[]): void {
|
|
19
|
+
this._classResources.set(cls, paths);
|
|
20
|
+
this._unclaimed.push(...paths);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Drain all unclaimed resource paths. Called by Module.captureRegistrations(). */
|
|
24
|
+
static drain(): string[] {
|
|
25
|
+
return this._unclaimed.splice(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Get resource paths for a specific class. */
|
|
29
|
+
static getResources(cls: Function): string[] {
|
|
30
|
+
return this._classResources.get(cls) ?? [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
export enum AccessType {
|
|
14
35
|
NONE = 0,
|
|
15
36
|
OFFLINE = 1 << 0,
|