From 2ce1e027531f5aa403dc7d375b56d474bb0334d7 Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Mon, 20 Apr 2026 16:28:17 +0800 Subject: [PATCH] fix: unblock startup on network and external font - Make license status snapshot non-blocking (revocation check in background) - Speed boot by not awaiting license network and capping editor ready wait - Stop disabling GPU by default on Win packaged builds - Remove external font fetch; bundle local Inter Made-with: Cursor --- app/main/index.ts | 23 +++++++++++-------- app/main/license/licenseService.ts | 13 +++++++++-- app/renderer/shared/styles/variables.css | 2 +- app/renderer/shared/ui/globals.css | 2 +- .../licenseService.networkRegression.test.ts | 14 +++++++++++ package-lock.json | 15 +++++++++--- package.json | 3 ++- 7 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 app/shared/license/licenseService.networkRegression.test.ts diff --git a/app/main/index.ts b/app/main/index.ts index 83f6fe2..2703bb2 100644 --- a/app/main/index.ts +++ b/app/main/index.ts @@ -28,10 +28,10 @@ import { } from './windows/createWindows'; /** - * На части конфигураций Windows окно Electron с `file://` остаётся чёрным из‑за GPU/композитора. - * Отключаем аппаратное ускорение в упакованном приложении; отключить обход: `DND_DISABLE_GPU=0`. + * Отключение GPU ломает скорость вторичных окон (презентация/пульт — WebGL). По умолчанию не трогаем. + * При чёрном экране в упакованной сборке: `DND_DISABLE_GPU=1`. */ -if (process.platform === 'win32' && app.isPackaged && process.env.DND_DISABLE_GPU !== '0') { +if (process.platform === 'win32' && app.isPackaged && process.env.DND_DISABLE_GPU === '1') { app.disableHardwareAcceleration(); } @@ -144,17 +144,20 @@ async function runStartupAfterHandlers(licenseService: LicenseService): Promise< console.error('[boot] ensureRoots', e); } - setBootWindowStatus(splash, 'Устанавливаем связь…'); setBootWindowStatus(splash, 'Проверка лицензии…'); - try { - await licenseService.getStatus(); - } catch (e) { - console.error('[boot] license getStatus', e); - } + // Сеть в `getStatus()` не блокируем старт: синхронный снимок, отзыв — в фоне. + licenseService.getStatusSync(); + queueMicrotask(() => { + licenseService.getStatus(); + }); setBootWindowStatus(splash, 'Загрузка редактора…'); const editor = createEditorWindowDeferred(); - await waitForEditorWindowReady(editor); + const bootEditorMs = 2000; + await Promise.race([ + waitForEditorWindowReady(editor), + new Promise((resolve) => setTimeout(resolve, bootEditorMs)), + ]); setBootWindowStatus(splash, 'Готово'); destroyBootWindow(splash); if (!editor.isDestroyed()) { diff --git a/app/main/license/licenseService.ts b/app/main/license/licenseService.ts index 6db46db..fccf9b6 100644 --- a/app/main/license/licenseService.ts +++ b/app/main/license/licenseService.ts @@ -113,12 +113,17 @@ export class LicenseService { return normalizeLicenseTokenInput(token); } + /** + * Онлайн-проверка отзыва. Не вызывать через `await` из UI-пути: без VPN/DNS до сервера + * лицензий TCP может висеть до таймаута (см. fetch), из‑за чего главное окно долго «чёрное». + */ private async maybeRefreshRemoteRevocation(payload: LicensePayloadV1): Promise { const base = process.env.DND_LICENSE_STATUS_URL?.trim(); if (!base) return; const now = Date.now(); if (now - this.lastRemoteRevokeCheckMs < 60_000) return; this.lastRemoteRevokeCheckMs = now; + const wasRevoked = this.lastRemoteRevoked; try { const u = new URL('v1/status', base.endsWith('/') ? base : `${base}/`); u.searchParams.set('sub', payload.sub); @@ -129,6 +134,9 @@ export class LicenseService { } catch { /* offline: не блокируем */ } + if (wasRevoked !== this.lastRemoteRevoked) { + emitLicenseStatusChanged(); + } } getStatusSync(): LicenseSnapshot { @@ -212,7 +220,8 @@ export class LicenseService { }; } - async getStatus(): Promise { + /** Снимок для UI/IPC: без ожидания сети (проверка отзыва уходит в фон). */ + getStatus(): LicenseSnapshot { if (this.isSkipLicense()) return this.getStatusSync(); const base = this.getStatusSync(); if (!base.active || !base.summary) return base; @@ -223,7 +232,7 @@ export class LicenseService { deviceId: this.deviceId, }); if (!v.ok) return this.getStatusSync(); - await this.maybeRefreshRemoteRevocation(v.payload); + void this.maybeRefreshRemoteRevocation(v.payload); return this.getStatusSync(); } diff --git a/app/renderer/shared/styles/variables.css b/app/renderer/shared/styles/variables.css index 9896199..fb4e781 100644 --- a/app/renderer/shared/styles/variables.css +++ b/app/renderer/shared/styles/variables.css @@ -85,7 +85,7 @@ /* --- Типографика --- */ --font: - 'Nimbus Sans', 'Nimbus Sans L', 'Nimbus Sans OT', 'Nimbus Sans PS', ui-sans-serif, system-ui, + Inter, 'Nimbus Sans', 'Nimbus Sans L', 'Nimbus Sans OT', 'Nimbus Sans PS', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, 'Apple Color Emoji', 'Segoe UI Emoji'; --text-xs: 12px; --text-sm: 13px; diff --git a/app/renderer/shared/ui/globals.css b/app/renderer/shared/ui/globals.css index bae82b9..f7b0758 100644 --- a/app/renderer/shared/ui/globals.css +++ b/app/renderer/shared/ui/globals.css @@ -1,5 +1,5 @@ @import '../styles/variables.css'; -@import url('https://fonts.cdnfonts.com/css/nimbus-sans'); +@import '@fontsource/inter/latin.css'; * { box-sizing: border-box; diff --git a/app/shared/license/licenseService.networkRegression.test.ts b/app/shared/license/licenseService.networkRegression.test.ts new file mode 100644 index 0000000..2eb871c --- /dev/null +++ b/app/shared/license/licenseService.networkRegression.test.ts @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); + +/** Регресс: `await` сетевой проверки отзыва блокировал IPC `license.getStatus` до 8 с без доступа к серверу. */ +void test('licenseService: getStatus не ждёт await проверки отзыва', () => { + const src = fs.readFileSync(path.join(root, 'app', 'main', 'license', 'licenseService.ts'), 'utf8'); + assert.match(src, /void this\.maybeRefreshRemoteRevocation\(/); + assert.doesNotMatch(src, /await this\.maybeRefreshRemoteRevocation\(/); +}); diff --git a/package-lock.json b/package-lock.json index dd44f9d..18a6a6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { - "name": "dnd_player", + "name": "DndGamePlayer", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "dnd_player", + "name": "DndGamePlayer", "version": "1.0.0", "license": "ISC", "dependencies": { + "@fontsource/inter": "^5.2.8", "pixi.js": "^8.18.1", "react": "^19.2.5", "react-dom": "^19.2.5", @@ -1410,6 +1411,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -10144,7 +10154,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 33b4c82..0562857 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build:obfuscate": "node scripts/build.mjs --production --obfuscate", "lint": "eslint . --max-warnings 0", "typecheck": "tsc -p tsconfig.eslint.json --noEmit", - "test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/project/assetPrune.test.ts app/main/project/zipRead.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/main/license/verifyLicenseToken.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs", + "test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/project/assetPrune.test.ts app/main/project/zipRead.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs", "format": "prettier . --check", "format:write": "prettier . --write", "release:info": "node scripts/print-release-info.mjs", @@ -24,6 +24,7 @@ "license": "ISC", "type": "module", "dependencies": { + "@fontsource/inter": "^5.2.8", "pixi.js": "^8.18.1", "react": "^19.2.5", "react-dom": "^19.2.5",