tabminal 3.0.34 → 3.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/app.js +15 -2
- package/public/index.html +175 -0
- package/public/sw.js +54 -7
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -17967,7 +17967,20 @@ document.addEventListener('keydown', (e) => {
|
|
|
17967
17967
|
}, true); // Use capture phase to override editor/terminal
|
|
17968
17968
|
|
|
17969
17969
|
|
|
17970
|
+
async function bootApp() {
|
|
17971
|
+
try {
|
|
17972
|
+
bootstrapServers();
|
|
17973
|
+
await initApp();
|
|
17974
|
+
window.__tabminalMarkBootSuccess?.();
|
|
17975
|
+
} catch (error) {
|
|
17976
|
+
console.error('[Boot] Failed to start Tabminal:', error);
|
|
17977
|
+
window.__tabminalMarkBootFailure?.(
|
|
17978
|
+
error?.message || 'app initialization failed'
|
|
17979
|
+
);
|
|
17980
|
+
throw error;
|
|
17981
|
+
}
|
|
17982
|
+
}
|
|
17983
|
+
|
|
17970
17984
|
// Start the app
|
|
17971
|
-
|
|
17972
|
-
initApp();
|
|
17985
|
+
void bootApp();
|
|
17973
17986
|
// #endregion
|
package/public/index.html
CHANGED
|
@@ -277,8 +277,20 @@
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
const runtimeStorageKey = 'tabminal_runtime_boot_id';
|
|
280
|
+
const bootRetryStorageKey = 'tabminal_boot_retry_state';
|
|
280
281
|
const versionApiUrl = './api/version';
|
|
281
282
|
const runtimeVersionTimeoutMs = 3000;
|
|
283
|
+
const bootWatchdogTimeoutMs = 25000;
|
|
284
|
+
const bootRecoveryProbeIntervalMs = 15000;
|
|
285
|
+
const bootRecoveryReloadDelayMs = 1200;
|
|
286
|
+
const bootRetryWindowMs = 90000;
|
|
287
|
+
const bootMaxReloadsPerWindow = 3;
|
|
288
|
+
window.__tabminalBootState = {
|
|
289
|
+
status: 'pending',
|
|
290
|
+
startedAt: Date.now(),
|
|
291
|
+
assetKey: '',
|
|
292
|
+
error: ''
|
|
293
|
+
};
|
|
282
294
|
const readStoredRuntimeBootId = () => {
|
|
283
295
|
try {
|
|
284
296
|
return localStorage.getItem(runtimeStorageKey) || '';
|
|
@@ -297,6 +309,159 @@
|
|
|
297
309
|
return readStoredRuntimeBootId() || `cold-${Date.now()}`;
|
|
298
310
|
};
|
|
299
311
|
window.__tabminalRuntimeAssetKey = getStartupFallbackAssetKey();
|
|
312
|
+
window.__tabminalBootState.assetKey =
|
|
313
|
+
window.__tabminalRuntimeAssetKey;
|
|
314
|
+
|
|
315
|
+
const readBootRetryState = () => {
|
|
316
|
+
try {
|
|
317
|
+
const parsed = JSON.parse(
|
|
318
|
+
sessionStorage.getItem(bootRetryStorageKey) || '{}'
|
|
319
|
+
);
|
|
320
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
321
|
+
return { firstAt: 0, count: 0 };
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
firstAt: Number.isFinite(parsed.firstAt)
|
|
325
|
+
? parsed.firstAt
|
|
326
|
+
: 0,
|
|
327
|
+
count: Number.isFinite(parsed.count)
|
|
328
|
+
? parsed.count
|
|
329
|
+
: 0
|
|
330
|
+
};
|
|
331
|
+
} catch {
|
|
332
|
+
return { firstAt: 0, count: 0 };
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const writeBootRetryState = (state) => {
|
|
337
|
+
try {
|
|
338
|
+
sessionStorage.setItem(
|
|
339
|
+
bootRetryStorageKey,
|
|
340
|
+
JSON.stringify(state)
|
|
341
|
+
);
|
|
342
|
+
} catch {
|
|
343
|
+
// Ignore storage failures; the watchdog can still probe.
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const clearBootRetryState = () => {
|
|
348
|
+
try {
|
|
349
|
+
sessionStorage.removeItem(bootRetryStorageKey);
|
|
350
|
+
} catch {
|
|
351
|
+
// Ignore storage failures.
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const canReloadForBootFailure = () => {
|
|
356
|
+
const now = Date.now();
|
|
357
|
+
const retryState = readBootRetryState();
|
|
358
|
+
const firstAt = (
|
|
359
|
+
retryState.firstAt
|
|
360
|
+
&& now - retryState.firstAt <= bootRetryWindowMs
|
|
361
|
+
)
|
|
362
|
+
? retryState.firstAt
|
|
363
|
+
: now;
|
|
364
|
+
const count = firstAt === retryState.firstAt
|
|
365
|
+
? retryState.count
|
|
366
|
+
: 0;
|
|
367
|
+
if (count >= bootMaxReloadsPerWindow) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
writeBootRetryState({
|
|
371
|
+
firstAt,
|
|
372
|
+
count: count + 1
|
|
373
|
+
});
|
|
374
|
+
return true;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const probeRuntimeReachable = async () => {
|
|
378
|
+
const controller = (
|
|
379
|
+
typeof AbortController === 'function'
|
|
380
|
+
? new AbortController()
|
|
381
|
+
: null
|
|
382
|
+
);
|
|
383
|
+
const timeoutId = window.setTimeout(() => {
|
|
384
|
+
if (controller) {
|
|
385
|
+
controller.abort();
|
|
386
|
+
}
|
|
387
|
+
}, runtimeVersionTimeoutMs);
|
|
388
|
+
try {
|
|
389
|
+
const response = await fetch(versionApiUrl, {
|
|
390
|
+
method: 'GET',
|
|
391
|
+
cache: 'no-store',
|
|
392
|
+
credentials: 'same-origin',
|
|
393
|
+
headers: {
|
|
394
|
+
'accept': 'application/json'
|
|
395
|
+
},
|
|
396
|
+
signal: controller?.signal
|
|
397
|
+
});
|
|
398
|
+
return response.ok;
|
|
399
|
+
} catch {
|
|
400
|
+
return false;
|
|
401
|
+
} finally {
|
|
402
|
+
window.clearTimeout(timeoutId);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const scheduleBootRecovery = (() => {
|
|
407
|
+
let timer = 0;
|
|
408
|
+
return (reason, delay = bootRecoveryReloadDelayMs) => {
|
|
409
|
+
const state = window.__tabminalBootState;
|
|
410
|
+
if (!state || state.status === 'success') {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
state.status = 'recovering';
|
|
414
|
+
state.error = String(reason || 'boot timeout');
|
|
415
|
+
if (timer) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
timer = window.setTimeout(async () => {
|
|
419
|
+
timer = 0;
|
|
420
|
+
if (state.status === 'success') {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const reachable = await probeRuntimeReachable();
|
|
424
|
+
if (!reachable) {
|
|
425
|
+
scheduleBootRecovery(
|
|
426
|
+
'runtime unavailable',
|
|
427
|
+
bootRecoveryProbeIntervalMs
|
|
428
|
+
);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!canReloadForBootFailure()) {
|
|
432
|
+
state.status = 'failed';
|
|
433
|
+
console.warn(
|
|
434
|
+
'[Boot] App shell failed after repeated reloads.',
|
|
435
|
+
state.error
|
|
436
|
+
);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
window.location.reload();
|
|
440
|
+
}, delay);
|
|
441
|
+
};
|
|
442
|
+
})();
|
|
443
|
+
|
|
444
|
+
window.__tabminalMarkBootSuccess = () => {
|
|
445
|
+
const state = window.__tabminalBootState;
|
|
446
|
+
if (state) {
|
|
447
|
+
state.status = 'success';
|
|
448
|
+
state.completedAt = Date.now();
|
|
449
|
+
state.error = '';
|
|
450
|
+
}
|
|
451
|
+
clearBootRetryState();
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
window.__tabminalMarkBootFailure = (reason) => {
|
|
455
|
+
scheduleBootRecovery(reason || 'boot failure');
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
window.setTimeout(() => {
|
|
459
|
+
const state = window.__tabminalBootState;
|
|
460
|
+
if (!state || state.status === 'success') {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
scheduleBootRecovery('boot watchdog timeout');
|
|
464
|
+
}, bootWatchdogTimeoutMs);
|
|
300
465
|
|
|
301
466
|
window.__tabminalResolveRuntimeVersion = (() => {
|
|
302
467
|
let promise = null;
|
|
@@ -704,10 +869,20 @@
|
|
|
704
869
|
const link = document.createElement('link');
|
|
705
870
|
link.rel = 'stylesheet';
|
|
706
871
|
link.href = `./styles.css?v=${encodeURIComponent(runtimeBootId)}`;
|
|
872
|
+
link.onerror = () => {
|
|
873
|
+
if (window.__tabminalMarkBootFailure) {
|
|
874
|
+
window.__tabminalMarkBootFailure('stylesheet load failed');
|
|
875
|
+
}
|
|
876
|
+
};
|
|
707
877
|
document.head.appendChild(link);
|
|
708
878
|
const script = document.createElement('script');
|
|
709
879
|
script.type = 'module';
|
|
710
880
|
script.src = `./app.js?v=${encodeURIComponent(runtimeBootId)}`;
|
|
881
|
+
script.onerror = () => {
|
|
882
|
+
if (window.__tabminalMarkBootFailure) {
|
|
883
|
+
window.__tabminalMarkBootFailure('app module load failed');
|
|
884
|
+
}
|
|
885
|
+
};
|
|
711
886
|
document.body.appendChild(script);
|
|
712
887
|
})();
|
|
713
888
|
|
package/public/sw.js
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
const WORKER_VERSION = new URL(self.location.href).searchParams.get('rt') || 'stable';
|
|
2
2
|
const CACHE_NAME = `tabminal-cache-${WORKER_VERSION}`;
|
|
3
|
+
const versioned = (path) => `${path}?v=${encodeURIComponent(WORKER_VERSION)}`;
|
|
3
4
|
const STATIC_ASSETS = [
|
|
5
|
+
'/',
|
|
6
|
+
'/index.html',
|
|
4
7
|
'/favicon.svg',
|
|
8
|
+
'/favicon_adaptive.svg',
|
|
5
9
|
'/manifest.json',
|
|
6
|
-
'/apple-touch-icon.png'
|
|
10
|
+
'/apple-touch-icon.png',
|
|
11
|
+
'/android-chrome-192x192.png',
|
|
12
|
+
'/android-chrome-192x192-any.png',
|
|
13
|
+
'/android-chrome-512x512.png',
|
|
14
|
+
'/android-chrome-512x512-any.png',
|
|
15
|
+
'/fonts/MonaspaceNeon-Regular.woff2',
|
|
16
|
+
'/fonts/MonaspaceNeon-Bold.woff2',
|
|
17
|
+
'/icons/map.json'
|
|
18
|
+
];
|
|
19
|
+
const VERSIONED_APP_ASSETS = [
|
|
20
|
+
versioned('/styles.css'),
|
|
21
|
+
versioned('/app.js'),
|
|
22
|
+
versioned('/modules/notifications.js'),
|
|
23
|
+
versioned('/modules/session-meta.js'),
|
|
24
|
+
versioned('/modules/url-auth.js')
|
|
7
25
|
];
|
|
8
26
|
|
|
9
27
|
async function networkFirst(request) {
|
|
10
28
|
const cache = await caches.open(CACHE_NAME);
|
|
11
29
|
try {
|
|
12
30
|
const response = await fetch(request);
|
|
13
|
-
|
|
31
|
+
if (response.ok) {
|
|
32
|
+
cache.put(request, response.clone());
|
|
33
|
+
}
|
|
14
34
|
return response;
|
|
15
35
|
} catch (_err) {
|
|
16
36
|
const cached = await cache.match(request);
|
|
@@ -24,15 +44,27 @@ async function cacheFirst(request) {
|
|
|
24
44
|
const cached = await cache.match(request);
|
|
25
45
|
if (cached) return cached;
|
|
26
46
|
const response = await fetch(request);
|
|
27
|
-
|
|
47
|
+
if (response.ok) {
|
|
48
|
+
cache.put(request, response.clone());
|
|
49
|
+
}
|
|
28
50
|
return response;
|
|
29
51
|
}
|
|
30
52
|
|
|
53
|
+
async function addAllSettled(cache, assets) {
|
|
54
|
+
await Promise.allSettled(
|
|
55
|
+
assets.map((asset) => cache.add(asset))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
self.addEventListener('install', event => {
|
|
32
60
|
self.skipWaiting();
|
|
33
|
-
event.waitUntil(
|
|
34
|
-
caches.open(CACHE_NAME)
|
|
35
|
-
|
|
61
|
+
event.waitUntil((async () => {
|
|
62
|
+
const cache = await caches.open(CACHE_NAME);
|
|
63
|
+
await addAllSettled(cache, [
|
|
64
|
+
...STATIC_ASSETS,
|
|
65
|
+
...VERSIONED_APP_ASSETS
|
|
66
|
+
]);
|
|
67
|
+
})());
|
|
36
68
|
});
|
|
37
69
|
|
|
38
70
|
self.addEventListener('activate', event => {
|
|
@@ -81,8 +113,23 @@ self.addEventListener('fetch', event => {
|
|
|
81
113
|
|| url.pathname === '/sw.js'
|
|
82
114
|
|| url.pathname.startsWith('/modules/')
|
|
83
115
|
);
|
|
116
|
+
const isVersionedAppShell = (
|
|
117
|
+
isAppShell
|
|
118
|
+
&& (
|
|
119
|
+
url.searchParams.get('v') === WORKER_VERSION
|
|
120
|
+
|| url.searchParams.get('rt') === WORKER_VERSION
|
|
121
|
+
)
|
|
122
|
+
);
|
|
84
123
|
|
|
85
|
-
if (isDocument
|
|
124
|
+
if (isDocument) {
|
|
125
|
+
event.respondWith(networkFirst(request));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (isVersionedAppShell) {
|
|
129
|
+
event.respondWith(cacheFirst(request));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (isAppShell) {
|
|
86
133
|
event.respondWith(networkFirst(request));
|
|
87
134
|
return;
|
|
88
135
|
}
|