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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabminal",
3
- "version": "3.0.34",
3
+ "version": "3.0.35",
4
4
  "description": "Tab(ter)minal, a Cloud-Native terminal and ACP agent workspace for desktop, tablet, and phone.",
5
5
  "type": "module",
6
6
  "bin": {
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
- bootstrapServers();
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
- cache.put(request, response.clone());
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
- cache.put(request, response.clone());
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).then(cache => cache.addAll(STATIC_ASSETS))
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 || isAppShell) {
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
  }