635 lines
19 KiB
TypeScript
635 lines
19 KiB
TypeScript
import { dirname } from "@std/path";
|
|
import { validateDocuments } from "./src/data.ts";
|
|
import { resolvePdfPath } from "./src/paths.ts";
|
|
import type { BuyerDocument } from "./src/types.ts";
|
|
import {
|
|
type AppSettings,
|
|
clampWindowSize,
|
|
DEFAULTS,
|
|
readLegacyConfig,
|
|
resolveCacheDir,
|
|
resolveSettingsPath,
|
|
saveSettings as saveAppSettings,
|
|
} from "./src/settings.ts";
|
|
import { cacheOrGetPdf } from "./src/pdf_cache.ts";
|
|
import INDEX_HTML from "./web/index.html" with { type: "text" };
|
|
import APP_JS from "./web/app.js" with { type: "text" };
|
|
import PDF_VIEWER_JS from "./web/pdf_viewer.js" with { type: "text" };
|
|
import STYLES_CSS from "./web/styles.css" with { type: "text" };
|
|
import SAMPLE_DOCUMENTS from "./sample-data/buyers_vision_anonymous.json" with {
|
|
type: "json",
|
|
};
|
|
|
|
const PREFIX = "[BizMatch QC]";
|
|
|
|
let settings: AppSettings = await initializeSettings();
|
|
let documents: BuyerDocument[] = [];
|
|
let loadError = "";
|
|
let dataSource: "sample" | "file" = "sample";
|
|
|
|
try {
|
|
const loaded = await loadDocumentsForMode();
|
|
documents = loaded.documents;
|
|
dataSource = loaded.source;
|
|
} catch (error) {
|
|
loadError = errorMessage(error);
|
|
console.error(`${PREFIX} Initial data load failed: ${loadError}`);
|
|
documents = validateDocuments(SAMPLE_DOCUMENTS);
|
|
dataSource = "sample";
|
|
}
|
|
|
|
async function initializeSettings(): Promise<AppSettings> {
|
|
const path = resolveSettingsPath();
|
|
console.log(`${PREFIX} Settings path: ${path}`);
|
|
|
|
let result: AppSettings;
|
|
let fileExisted = false;
|
|
|
|
try {
|
|
const content = await Deno.readTextFile(path);
|
|
result = JSON.parse(content) as unknown as AppSettings;
|
|
fileExisted = true;
|
|
} catch {
|
|
result = { ...DEFAULTS };
|
|
}
|
|
|
|
const validated = { ...DEFAULTS };
|
|
if (
|
|
fileExisted && result && typeof result === "object" &&
|
|
!Array.isArray(result)
|
|
) {
|
|
const obj = result as unknown as Record<string, unknown>;
|
|
if (typeof obj.jsonPath === "string") validated.jsonPath = obj.jsonPath;
|
|
if (typeof obj.pdfBaseDirectory === "string") {
|
|
validated.pdfBaseDirectory = obj.pdfBaseDirectory;
|
|
}
|
|
if (typeof obj.useAnonymousData === "boolean") {
|
|
validated.useAnonymousData = obj.useAnonymousData;
|
|
}
|
|
if (
|
|
typeof obj.windowWidth === "number" && !isNaN(obj.windowWidth) &&
|
|
obj.windowWidth >= 1100
|
|
) {
|
|
validated.windowWidth = obj.windowWidth;
|
|
}
|
|
if (
|
|
typeof obj.windowHeight === "number" && !isNaN(obj.windowHeight) &&
|
|
obj.windowHeight >= 700
|
|
) {
|
|
validated.windowHeight = obj.windowHeight;
|
|
}
|
|
console.log(`${PREFIX} Settings loaded successfully.`);
|
|
} else {
|
|
console.log(`${PREFIX} No existing settings file found. Using defaults.`);
|
|
}
|
|
|
|
if (!fileExisted) {
|
|
const legacy = await readLegacyConfig();
|
|
if (legacy) {
|
|
validated.jsonPath = validated.jsonPath || legacy.jsonPath;
|
|
validated.pdfBaseDirectory = validated.pdfBaseDirectory ||
|
|
legacy.pdfBaseDirectory;
|
|
await saveAppSettings(validated);
|
|
}
|
|
}
|
|
|
|
return validated;
|
|
}
|
|
|
|
function errorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
|
|
async function loadDocumentsFromFile(
|
|
jsonPath: string,
|
|
): Promise<{ documents: BuyerDocument[]; source: "file" }> {
|
|
let raw: string;
|
|
try {
|
|
raw = await Deno.readTextFile(jsonPath);
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Cannot read JSON file "${jsonPath}": ${errorMessage(error)}`,
|
|
);
|
|
}
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch (error) {
|
|
throw new Error(`Invalid JSON in "${jsonPath}": ${errorMessage(error)}`);
|
|
}
|
|
try {
|
|
return { documents: validateDocuments(parsed), source: "file" };
|
|
} catch (error) {
|
|
throw new Error(
|
|
`JSON validation failed for "${jsonPath}": ${errorMessage(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function loadDocumentsForMode(): Promise<
|
|
{ documents: BuyerDocument[]; source: "sample" | "file" }
|
|
> {
|
|
if (settings.useAnonymousData) {
|
|
return { documents: validateDocuments(SAMPLE_DOCUMENTS), source: "sample" };
|
|
}
|
|
if (!settings.jsonPath.trim()) {
|
|
return { documents: validateDocuments(SAMPLE_DOCUMENTS), source: "sample" };
|
|
}
|
|
return await loadDocumentsFromFile(settings.jsonPath);
|
|
}
|
|
|
|
async function applySettings(
|
|
next: {
|
|
jsonPath?: string;
|
|
pdfBaseDirectory?: string;
|
|
useAnonymousData?: boolean;
|
|
},
|
|
): Promise<void> {
|
|
const newSettings: AppSettings = {
|
|
...settings,
|
|
jsonPath: next.jsonPath !== undefined
|
|
? next.jsonPath.trim()
|
|
: settings.jsonPath,
|
|
pdfBaseDirectory: next.pdfBaseDirectory !== undefined
|
|
? next.pdfBaseDirectory.trim()
|
|
: settings.pdfBaseDirectory,
|
|
useAnonymousData: next.useAnonymousData !== undefined
|
|
? next.useAnonymousData
|
|
: settings.useAnonymousData,
|
|
};
|
|
|
|
if (newSettings.useAnonymousData) {
|
|
const docs = validateDocuments(SAMPLE_DOCUMENTS);
|
|
settings = newSettings;
|
|
documents = docs;
|
|
dataSource = "sample";
|
|
loadError = "";
|
|
await saveAppSettings(settings);
|
|
console.log(
|
|
`${PREFIX} Switched to anonymized sample data (${docs.length} documents).`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!newSettings.jsonPath.trim()) {
|
|
settings = newSettings;
|
|
documents = validateDocuments(SAMPLE_DOCUMENTS);
|
|
dataSource = "sample";
|
|
loadError = "";
|
|
await saveAppSettings(settings);
|
|
console.log(`${PREFIX} No JSON path configured; using sample data.`);
|
|
return;
|
|
}
|
|
|
|
const loaded = await loadDocumentsFromFile(newSettings.jsonPath);
|
|
|
|
if (newSettings.pdfBaseDirectory) {
|
|
try {
|
|
const stat = await Deno.stat(newSettings.pdfBaseDirectory);
|
|
if (!stat.isDirectory) {
|
|
throw new Error("Path exists but is not a directory.");
|
|
}
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Cannot access PDF base directory "${newSettings.pdfBaseDirectory}": ${
|
|
errorMessage(error)
|
|
}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
await saveAppSettings(newSettings);
|
|
settings = newSettings;
|
|
documents = loaded.documents;
|
|
dataSource = loaded.source;
|
|
loadError = "";
|
|
console.log(
|
|
`${PREFIX} Loaded ${documents.length} documents from ${settings.jsonPath}.`,
|
|
);
|
|
if (settings.pdfBaseDirectory) {
|
|
console.log(`${PREFIX} PDF base directory: ${settings.pdfBaseDirectory}`);
|
|
}
|
|
}
|
|
|
|
function json(data: unknown, status = 200) {
|
|
return Response.json(data, {
|
|
status,
|
|
headers: { "cache-control": "no-store" },
|
|
});
|
|
}
|
|
|
|
// ---- PDF token system ----
|
|
// A token stores a resolved cache path so that Range requests
|
|
// (which PDF.js makes many of) hit the local disk cache directly
|
|
// without re-stat'ing or re-validating against NAS for each one.
|
|
|
|
interface PdfToken {
|
|
cachePath: string;
|
|
sourcePath: string;
|
|
fileSize: number;
|
|
stale: boolean;
|
|
createdAt: number;
|
|
}
|
|
|
|
const pdfTokens = new Map<string, PdfToken>();
|
|
const TOKEN_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
|
|
function cleanupTokens() {
|
|
const now = Date.now();
|
|
for (const [token, entry] of pdfTokens) {
|
|
if (now - entry.createdAt > TOKEN_TTL_MS) pdfTokens.delete(token);
|
|
}
|
|
}
|
|
// Periodic cleanup every 10 minutes
|
|
setInterval(cleanupTokens, 10 * 60 * 1000);
|
|
|
|
async function servePdfBytes(
|
|
cachePath: string,
|
|
fileSize: number,
|
|
rangeHeader: string | null,
|
|
elapsedStart: number,
|
|
stale: boolean,
|
|
): Promise<Response> {
|
|
if (rangeHeader) {
|
|
const match = rangeHeader.match(/^bytes=(\d+)-(\d*)$/);
|
|
if (match) {
|
|
const rangeStart = parseInt(match[1], 10);
|
|
const rangeEnd = match[2] ? parseInt(match[2], 10) : fileSize - 1;
|
|
const length = rangeEnd - rangeStart + 1;
|
|
|
|
if (rangeStart < 0 || rangeEnd >= fileSize || rangeStart > rangeEnd) {
|
|
return new Response(null, {
|
|
status: 416,
|
|
headers: { "content-range": `bytes */${fileSize}` },
|
|
});
|
|
}
|
|
|
|
const file = await Deno.open(cachePath, { read: true });
|
|
await file.seek(rangeStart, Deno.SeekMode.Start);
|
|
const buf = new Uint8Array(length);
|
|
let bytesRead = 0;
|
|
while (bytesRead < length) {
|
|
const n = await file.read(buf.subarray(bytesRead));
|
|
if (n === null) break;
|
|
bytesRead += n;
|
|
}
|
|
file.close();
|
|
|
|
const elapsed = Math.round(performance.now() - elapsedStart);
|
|
console.log(
|
|
`${PREFIX} PDF range served from local cache: bytes ${rangeStart}-${
|
|
rangeStart + bytesRead - 1
|
|
}/${fileSize} in ${elapsed}ms`,
|
|
);
|
|
|
|
return new Response(buf.slice(0, bytesRead), {
|
|
status: 206,
|
|
headers: {
|
|
"content-type": "application/pdf",
|
|
"content-length": String(bytesRead),
|
|
"content-range": `bytes ${rangeStart}-${
|
|
rangeStart + bytesRead - 1
|
|
}/${fileSize}`,
|
|
"accept-ranges": "bytes",
|
|
...(stale ? { "x-bizmatch-cache-status": "stale" } : {}),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const file = await Deno.open(cachePath, { read: true });
|
|
const elapsed = Math.round(performance.now() - elapsedStart);
|
|
console.log(
|
|
`${PREFIX} PDF full served from local cache (${fileSize}B) in ${elapsed}ms`,
|
|
);
|
|
return new Response(file.readable, {
|
|
headers: {
|
|
"content-type": "application/pdf",
|
|
"content-length": String(fileSize),
|
|
"accept-ranges": "bytes",
|
|
...(stale ? { "x-bizmatch-cache-status": "stale" } : {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
// ------- HTTP Server -------
|
|
|
|
Deno.serve(async (request) => {
|
|
const url = new URL(request.url);
|
|
|
|
if (url.pathname === "/api/state" && request.method === "GET") {
|
|
const configForClient = {
|
|
jsonPath: settings.jsonPath,
|
|
pdfBaseDirectory: settings.pdfBaseDirectory,
|
|
useAnonymousData: settings.useAnonymousData,
|
|
};
|
|
return json({ config: configForClient, documents, loadError, dataSource });
|
|
}
|
|
|
|
if (url.pathname === "/api/config" && request.method === "POST") {
|
|
try {
|
|
interface ConfigBody {
|
|
jsonPath?: string;
|
|
pdfBaseDirectory?: string;
|
|
useAnonymousData?: boolean;
|
|
}
|
|
const body = await request.json() as ConfigBody;
|
|
await applySettings({
|
|
jsonPath: body.jsonPath ?? settings.jsonPath,
|
|
pdfBaseDirectory: body.pdfBaseDirectory ?? settings.pdfBaseDirectory,
|
|
useAnonymousData: body.useAnonymousData ?? settings.useAnonymousData,
|
|
});
|
|
return json({ ok: true, count: documents.length, dataSource });
|
|
} catch (error) {
|
|
const message = errorMessage(error);
|
|
loadError = message;
|
|
console.error(`${PREFIX} Settings rejected: ${message}`);
|
|
return json({ error: message }, 400);
|
|
}
|
|
}
|
|
|
|
// ---- Client-side log forwarding ----
|
|
if (url.pathname === "/api/client-log" && request.method === "POST") {
|
|
try {
|
|
const body = await request.json() as {
|
|
level?: string;
|
|
message?: string;
|
|
data?: unknown;
|
|
};
|
|
const level = body.level || "info";
|
|
const prefix = `${PREFIX} Client`;
|
|
if (level === "error") {
|
|
console.error(
|
|
`${prefix} [ERROR] ${body.message || ""}`,
|
|
body.data || "",
|
|
);
|
|
} else if (level === "warn") {
|
|
console.warn(`${prefix} [WARN] ${body.message || ""}`, body.data || "");
|
|
} else {
|
|
console.log(`${prefix} ${body.message || ""}`, body.data || "");
|
|
}
|
|
} catch { /* ignore */ }
|
|
return new Response(null, { status: 204 });
|
|
}
|
|
|
|
// ---- PDF prepare endpoint ----
|
|
if (url.pathname === "/api/pdf/prepare" && request.method === "POST") {
|
|
try {
|
|
const body = await request.json() as {
|
|
index?: number;
|
|
requestId?: string;
|
|
};
|
|
const index = body.index;
|
|
const doc = documents[index!];
|
|
if (!Number.isInteger(index) || !doc) {
|
|
return json({ error: "Unknown document index." }, 404);
|
|
}
|
|
|
|
const sourcePath = resolvePdfPath(settings.pdfBaseDirectory, doc);
|
|
const relative = doc._letter + "/" + doc.file_name;
|
|
const reqTag = body.requestId ? `PDF ${body.requestId}` : "PDF";
|
|
console.log(`${PREFIX} ${reqTag} prepare requested: ${relative}`);
|
|
|
|
const startTime = performance.now();
|
|
const cacheResult = await cacheOrGetPdf(sourcePath);
|
|
|
|
// Validate cached file
|
|
let fileSize: number;
|
|
try {
|
|
fileSize = (await Deno.stat(cacheResult.path)).size;
|
|
const f = await Deno.open(cacheResult.path, { read: true });
|
|
const head = new Uint8Array(5);
|
|
let n = 0;
|
|
while (n < 5) {
|
|
const r = await f.read(head.subarray(n));
|
|
if (r === null) break;
|
|
n += r;
|
|
}
|
|
f.close();
|
|
const header = new TextDecoder().decode(head.slice(0, n));
|
|
if (header !== "%PDF-") {
|
|
console.warn(
|
|
`${PREFIX} Cached PDF invalid header: "${header}", re-caching`,
|
|
);
|
|
await Deno.remove(cacheResult.path).catch(() => {});
|
|
const fresh = await cacheOrGetPdf(sourcePath);
|
|
fileSize = (await Deno.stat(fresh.path)).size;
|
|
}
|
|
} catch {
|
|
throw new Error("Cached file is not readable.");
|
|
}
|
|
|
|
const token = crypto.randomUUID();
|
|
pdfTokens.set(token, {
|
|
cachePath: cacheResult.path,
|
|
sourcePath,
|
|
fileSize,
|
|
stale: cacheResult.stale,
|
|
createdAt: Date.now(),
|
|
});
|
|
|
|
const elapsed = Math.round(performance.now() - startTime);
|
|
const status = cacheResult.stale ? "refresh" : "hit";
|
|
console.log(
|
|
`${PREFIX} ${reqTag} disk cache ${status} in ${elapsed}ms, token created`,
|
|
);
|
|
|
|
return json({
|
|
url: `/api/pdf/content/${token}`,
|
|
size: fileSize,
|
|
cacheStatus: status,
|
|
});
|
|
} catch (error) {
|
|
const message = errorMessage(error);
|
|
console.error(`${PREFIX} PDF prepare failed: ${message}`);
|
|
return json({ error: message }, 404);
|
|
}
|
|
}
|
|
|
|
// ---- PDF content endpoint (range-aware, cache-only) ----
|
|
if (
|
|
url.pathname.startsWith("/api/pdf/content/") && request.method === "GET"
|
|
) {
|
|
const token = url.pathname.slice("/api/pdf/content/".length);
|
|
const entry = pdfTokens.get(token);
|
|
if (!entry) {
|
|
return json({ error: "Invalid or expired PDF token." }, 404);
|
|
}
|
|
const startTime = performance.now();
|
|
try {
|
|
return await servePdfBytes(
|
|
entry.cachePath,
|
|
entry.fileSize,
|
|
request.headers.get("range"),
|
|
startTime,
|
|
entry.stale,
|
|
);
|
|
} catch (error) {
|
|
const message = errorMessage(error);
|
|
console.error(`${PREFIX} PDF content serve failed: ${message}`);
|
|
return json({ error: message }, 500);
|
|
}
|
|
}
|
|
|
|
// ---- HEAD request for pdf content ----
|
|
if (
|
|
url.pathname.startsWith("/api/pdf/content/") && request.method === "HEAD"
|
|
) {
|
|
const token = url.pathname.slice("/api/pdf/content/".length);
|
|
const entry = pdfTokens.get(token);
|
|
if (!entry) {
|
|
return new Response(null, { status: 404 });
|
|
}
|
|
return new Response(null, {
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/pdf",
|
|
"content-length": String(entry.fileSize),
|
|
"accept-ranges": "bytes",
|
|
},
|
|
});
|
|
}
|
|
|
|
// ---- Static files ----
|
|
const staticFiles: Record<string, { body: string; type: string }> = {
|
|
"/": { body: INDEX_HTML, type: "text/html; charset=utf-8" },
|
|
"/index.html": { body: INDEX_HTML, type: "text/html; charset=utf-8" },
|
|
"/app.js": { body: APP_JS, type: "text/javascript; charset=utf-8" },
|
|
"/pdf_viewer.js": {
|
|
body: PDF_VIEWER_JS,
|
|
type: "text/javascript; charset=utf-8",
|
|
},
|
|
"/styles.css": { body: STYLES_CSS, type: "text/css; charset=utf-8" },
|
|
};
|
|
const file = staticFiles[url.pathname];
|
|
if (file) {
|
|
return new Response(file.body, {
|
|
headers: { "content-type": file.type, "cache-control": "no-store" },
|
|
});
|
|
}
|
|
|
|
return new Response("Not found", { status: 404 });
|
|
});
|
|
|
|
// ------- Startup logging -------
|
|
console.log(`${PREFIX} Settings path: ${resolveSettingsPath()}`);
|
|
console.log(`${PREFIX} PDF cache directory: ${resolveCacheDir()}`);
|
|
console.log(
|
|
`${PREFIX} Data mode: ${
|
|
settings.useAnonymousData
|
|
? "anonymized sample"
|
|
: (settings.jsonPath ? `file (${settings.jsonPath})` : "embedded sample")
|
|
}`,
|
|
);
|
|
console.log(`${PREFIX} Documents loaded: ${documents.length}`);
|
|
if (loadError) console.error(`${PREFIX} ${loadError}`);
|
|
|
|
// ------- Window management -------
|
|
const clamped = clampWindowSize(settings);
|
|
console.log(
|
|
`${PREFIX} Settings window size loaded: ${clamped.windowWidth}x${clamped.windowHeight}`,
|
|
);
|
|
console.log(
|
|
`${PREFIX} Creating main window with: ${clamped.windowWidth}x${clamped.windowHeight}`,
|
|
);
|
|
|
|
// Deno 2.9 Desktop API: use BrowserWindow constructor with width/height,
|
|
// then verify with getSize() and apply setSize() if needed.
|
|
type _Win = {
|
|
getSize(): [number, number];
|
|
setSize(w: number, h: number): void;
|
|
addEventListener(
|
|
type: string,
|
|
listener: (event: CustomEvent) => void,
|
|
): void;
|
|
show(): void;
|
|
};
|
|
|
|
const _WinCtor = (Deno as unknown as {
|
|
BrowserWindow: new (
|
|
opts?: { title?: string; width?: number; height?: number },
|
|
) => _Win;
|
|
}).BrowserWindow;
|
|
|
|
const win = new _WinCtor({
|
|
title: "BizMatch QC",
|
|
width: clamped.windowWidth,
|
|
height: clamped.windowHeight,
|
|
});
|
|
|
|
const [actualW, actualH] = win.getSize();
|
|
console.log(
|
|
`${PREFIX} Main window actual size after construction: ${actualW}x${actualH}`,
|
|
);
|
|
|
|
if (
|
|
actualW !== clamped.windowWidth || actualH !== clamped.windowHeight
|
|
) {
|
|
console.log(
|
|
`${PREFIX} Window size differs from requested, applying setSize(${clamped.windowWidth}, ${clamped.windowHeight})`,
|
|
);
|
|
win.setSize(clamped.windowWidth, clamped.windowHeight);
|
|
const [w2, h2] = win.getSize();
|
|
console.log(`${PREFIX} After setSize: ${w2}x${h2}`);
|
|
}
|
|
|
|
// Native resize tracking using Deno 2.9 BrowserWindow events
|
|
let resizeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
try {
|
|
win.addEventListener(
|
|
"resize",
|
|
((e: CustomEvent) => {
|
|
if (resizeTimer !== undefined) clearTimeout(resizeTimer);
|
|
// Store immediately but debounce writes
|
|
const w = e.detail?.width ?? 0;
|
|
const h = e.detail?.height ?? 0;
|
|
if (w >= 1100 && h >= 700) {
|
|
settings.windowWidth = w;
|
|
settings.windowHeight = h;
|
|
}
|
|
resizeTimer = setTimeout(() => {
|
|
if (settings.windowWidth >= 1100 && settings.windowHeight >= 700) {
|
|
console.log(
|
|
`${PREFIX} Window resized: ${settings.windowWidth}x${settings.windowHeight}`,
|
|
);
|
|
saveAppSettings(settings).catch((err) =>
|
|
console.error(
|
|
`${PREFIX} Failed to save window size: ${errorMessage(err)}`,
|
|
)
|
|
);
|
|
}
|
|
}, 500);
|
|
}) as (e: Event) => void,
|
|
);
|
|
|
|
win.addEventListener(
|
|
"close",
|
|
(() => {
|
|
if (resizeTimer !== undefined) clearTimeout(resizeTimer);
|
|
const [fw, fh] = win.getSize();
|
|
if (fw >= 1100 && fh >= 700) {
|
|
settings.windowWidth = fw;
|
|
settings.windowHeight = fh;
|
|
}
|
|
try {
|
|
const path = resolveSettingsPath();
|
|
Deno.mkdirSync(dirname(path), { recursive: true });
|
|
Deno.writeTextFileSync(path, JSON.stringify(settings, null, 2));
|
|
console.log(
|
|
`${PREFIX} Window size saved on close: ${settings.windowWidth}x${settings.windowHeight}`,
|
|
);
|
|
} catch (err) {
|
|
console.error(
|
|
`${PREFIX} Failed to save settings on close: ${errorMessage(err)}`,
|
|
);
|
|
}
|
|
}) as (e: Event) => void,
|
|
);
|
|
} catch (err) {
|
|
console.warn(
|
|
`${PREFIX} Native window-event binding unavailable: ${errorMessage(err)}`,
|
|
);
|
|
}
|
|
|
|
win.show();
|