@quicktvui/web-cli 3.0.0 → 3.1.1

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.
@@ -121,7 +121,7 @@
121
121
  left: 0;
122
122
  right: 0;
123
123
  z-index: 99998;
124
- background: rgba(0, 0, 0, 0.8);
124
+ background: rgba(0, 0, 0, 0.2);
125
125
  color: #aaa;
126
126
  font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
127
127
  font-size: 12px;
@@ -289,10 +289,175 @@
289
289
 
290
290
  // ========== SSE 热更新 ==========
291
291
  var sseConnected = false;
292
+ var sse = null;
293
+ var sseLeaderHeartbeat = null;
294
+ var sseLeaderPoller = null;
295
+ var sseTabId = 'qt-dev-tab-' + Date.now() + '-' + Math.random().toString(36).slice(2);
296
+ var sseLeaderKey = 'quicktvui-dev-sse-leader';
297
+ var sseLeaderTtl = 10000;
298
+ // 仅处理当前页面加载后发生的刷新事件,避免前后台切换时重放旧事件误刷新。
299
+ var pageLoadedAt = Date.now();
292
300
  // 页面加载后的冷却期:这段时间内忽略 bundle-update 事件
293
301
  // 防止 SSE 重连后收到缓存的旧 bundle-update 导致无限刷新
294
302
  var sseCooldownUntil = Date.now() + 3000; // 3 秒冷却期
295
303
 
304
+ function isStaleReloadEvent(data) {
305
+ if (!data || typeof data.timestamp !== 'number') return false;
306
+ return data.timestamp <= pageLoadedAt;
307
+ }
308
+
309
+ function readSseLeader() {
310
+ try {
311
+ var raw = localStorage.getItem(sseLeaderKey);
312
+ return raw ? JSON.parse(raw) : null;
313
+ } catch (e) {
314
+ return null;
315
+ }
316
+ }
317
+
318
+ function isSseLeaderAlive(leader) {
319
+ return !!(leader && leader.tabId && (Date.now() - leader.ts) < sseLeaderTtl);
320
+ }
321
+
322
+ function writeSseLeader() {
323
+ try {
324
+ localStorage.setItem(sseLeaderKey, JSON.stringify({
325
+ tabId: sseTabId,
326
+ ts: Date.now(),
327
+ href: window.location.href
328
+ }));
329
+ } catch (e) {
330
+ console.warn('[Dev] Failed to write SSE leader lock:', e);
331
+ }
332
+ }
333
+
334
+ function clearSseLeader() {
335
+ var leader = readSseLeader();
336
+ if (leader && leader.tabId === sseTabId) {
337
+ try {
338
+ localStorage.removeItem(sseLeaderKey);
339
+ } catch (e) {
340
+ console.warn('[Dev] Failed to clear SSE leader lock:', e);
341
+ }
342
+ }
343
+ }
344
+
345
+ function startSseHeartbeat() {
346
+ if (sseLeaderHeartbeat) return;
347
+ writeSseLeader();
348
+ sseLeaderHeartbeat = setInterval(function() {
349
+ writeSseLeader();
350
+ }, Math.max(2000, Math.floor(sseLeaderTtl / 2)));
351
+ }
352
+
353
+ function stopSseHeartbeat() {
354
+ if (sseLeaderHeartbeat) {
355
+ clearInterval(sseLeaderHeartbeat);
356
+ sseLeaderHeartbeat = null;
357
+ }
358
+ }
359
+
360
+ function closeSse(reason) {
361
+ stopSseHeartbeat();
362
+ if (sse) {
363
+ sse.close();
364
+ sse = null;
365
+ }
366
+ if (sseConnected) {
367
+ console.log('[Dev] SSE closed:', reason || 'manual');
368
+ }
369
+ sseConnected = false;
370
+ }
371
+
372
+ function shouldOwnSse() {
373
+ return document.visibilityState !== 'hidden' &&
374
+ (typeof document.hasFocus !== 'function' || document.hasFocus());
375
+ }
376
+
377
+ function becomeSseLeader() {
378
+ writeSseLeader();
379
+ startSseHeartbeat();
380
+ if (!sse) {
381
+ console.log('[Dev] SSE leader acquired by current tab');
382
+ connectSse();
383
+ }
384
+ }
385
+
386
+ function connectSse() {
387
+ if (sse || !window.__BUNDLE_CONFIG__ || !window.__BUNDLE_CONFIG__.watch) return;
388
+ try {
389
+ sse = new EventSource(window.__BUNDLE_CONFIG__.sseEndpoint);
390
+ sse.onopen = function() {
391
+ sseConnected = true;
392
+ console.log('[Dev] SSE connected');
393
+ updateStatus('ready', '已连接 - 等待变化');
394
+ };
395
+ sse.onmessage = function(e) {
396
+ try {
397
+ var data = JSON.parse(e.data);
398
+ console.log('[Dev] SSE event:', data.type, data);
399
+ if (data.type === 'connected') {
400
+ updateStatus('ready', '开发服务器已连接');
401
+ } else if (data.type === 'bundle-update') {
402
+ if (isStaleReloadEvent(data)) {
403
+ console.log('[Dev] Ignoring stale bundle-update', data.timestamp, '<=', pageLoadedAt);
404
+ updateStatus('ready', '构建完成');
405
+ return;
406
+ }
407
+ if (Date.now() < sseCooldownUntil) {
408
+ console.log('[Dev] Ignoring bundle-update during cooldown');
409
+ updateStatus('ready', '构建完成');
410
+ return;
411
+ }
412
+ updateStatus('building', 'Bundle 更新中...');
413
+ setTimeout(function() { location.reload(); }, 300);
414
+ } else if (data.type === 'full-reload') {
415
+ if (isStaleReloadEvent(data)) {
416
+ console.log('[Dev] Ignoring stale full-reload', data.timestamp, '<=', pageLoadedAt);
417
+ return;
418
+ }
419
+ if (Date.now() < sseCooldownUntil) {
420
+ console.log('[Dev] Ignoring full-reload during cooldown');
421
+ return;
422
+ }
423
+ updateStatus('building', '正在刷新...');
424
+ setTimeout(function() { location.reload(); }, 300);
425
+ } else if (data.type === 'build-status') {
426
+ if (data.status === 'building') {
427
+ updateStatus('building', data.message || '构建中...');
428
+ } else if (data.status === 'ready') {
429
+ updateStatus('ready', data.message || '构建完成');
430
+ } else if (data.status === 'error') {
431
+ updateStatus('error', data.message || '构建失败');
432
+ }
433
+ }
434
+ } catch (ex) {
435
+ console.error('[Dev] SSE parse error:', ex);
436
+ }
437
+ };
438
+ sse.onerror = function() {
439
+ sseConnected = false;
440
+ console.log('[Dev] SSE disconnected, retrying...');
441
+ updateStatus('building', 'SSE 断开,重连中...');
442
+ };
443
+ } catch (e) {
444
+ console.error('[Dev] SSE init error:', e);
445
+ }
446
+ }
447
+
448
+ function updateSseLeadership() {
449
+ if (!window.__BUNDLE_CONFIG__ || !window.__BUNDLE_CONFIG__.watch) return;
450
+
451
+ // 前台页优先:当前可见且有焦点的标签页主动接管 SSE。
452
+ if (!shouldOwnSse()) {
453
+ clearSseLeader();
454
+ closeSse('tab inactive');
455
+ return;
456
+ }
457
+
458
+ becomeSseLeader();
459
+ }
460
+
296
461
  // ========== CORS 代理 ==========
297
462
  // 在 web-cli dev 模式下,拦截所有跨域 fetch/XHR/Image 请求,
298
463
  // 转换为 /proxy/ 路径由 DevServer 代理转发,解决浏览器 CORS 限制
@@ -411,56 +576,29 @@
411
576
  })();
412
577
 
413
578
  if (window.__BUNDLE_CONFIG__ && window.__BUNDLE_CONFIG__.watch) {
414
- try {
415
- var sse = new EventSource(window.__BUNDLE_CONFIG__.sseEndpoint);
416
- sse.onopen = function() {
417
- sseConnected = true;
418
- console.log('[Dev] SSE connected');
419
- updateStatus('ready', '已连接 - 等待变化');
420
- };
421
- sse.onmessage = function(e) {
422
- try {
423
- var data = JSON.parse(e.data);
424
- console.log('[Dev] SSE event:', data.type, data);
425
- if (data.type === 'connected') {
426
- updateStatus('ready', '开发服务器已连接');
427
- } else if (data.type === 'bundle-update') {
428
- // 冷却期内忽略 bundle-update(可能是 SSE 重连后缓存的旧事件)
429
- if (Date.now() < sseCooldownUntil) {
430
- console.log('[Dev] Ignoring bundle-update during cooldown');
431
- updateStatus('ready', '构建完成');
432
- return;
433
- }
434
- updateStatus('building', 'Bundle 更新中...');
435
- setTimeout(function() { location.reload(); }, 300);
436
- } else if (data.type === 'full-reload') {
437
- if (Date.now() < sseCooldownUntil) {
438
- console.log('[Dev] Ignoring full-reload during cooldown');
439
- return;
440
- }
441
- updateStatus('building', '正在刷新...');
442
- setTimeout(function() { location.reload(); }, 300);
443
- } else if (data.type === 'build-status') {
444
- if (data.status === 'building') {
445
- updateStatus('building', data.message || '构建中...');
446
- } else if (data.status === 'ready') {
447
- updateStatus('ready', data.message || '构建完成');
448
- } else if (data.status === 'error') {
449
- updateStatus('error', data.message || '构建失败');
450
- }
451
- }
452
- } catch (ex) {
453
- console.error('[Dev] SSE parse error:', ex);
579
+ updateSseLeadership();
580
+ sseLeaderPoller = setInterval(updateSseLeadership, 3000);
581
+ window.addEventListener('storage', function(event) {
582
+ if (event.key === sseLeaderKey) {
583
+ if (shouldOwnSse()) {
584
+ setTimeout(updateSseLeadership, 0);
585
+ } else {
586
+ closeSse('another tab owns SSE');
587
+ updateStatus('ready', '热更新由其他标签页持有');
454
588
  }
455
- };
456
- sse.onerror = function() {
457
- sseConnected = false;
458
- console.log('[Dev] SSE disconnected, retrying...');
459
- updateStatus('building', 'SSE 断开,重连中...');
460
- };
461
- } catch (e) {
462
- console.error('[Dev] SSE init error:', e);
463
- }
589
+ }
590
+ });
591
+ window.addEventListener('focus', updateSseLeadership);
592
+ window.addEventListener('blur', updateSseLeadership);
593
+ document.addEventListener('visibilitychange', updateSseLeadership);
594
+ window.addEventListener('beforeunload', function() {
595
+ clearSseLeader();
596
+ closeSse('beforeunload');
597
+ });
598
+ window.addEventListener('pagehide', function() {
599
+ clearSseLeader();
600
+ closeSse('pagehide');
601
+ });
464
602
  }
465
603
 
466
604
  // ========== 自动添加 bundle 参数(如果 URL 中没有) ==========