From 09bdf0c0c716da7e857106891663a6cc1b8f4f05 Mon Sep 17 00:00:00 2001 From: ClaudiaFang Date: Tue, 14 Jul 2026 13:29:52 +0000 Subject: [PATCH] fix: satisfy Obsidian plugin scan (undescribed directive, popout-window timers) - src/utils/git-blob-sha.ts: add a required description to the sonarjs/hashing eslint-disable comment. - src/services/github-service.ts, src/settings.ts: use window.setTimeout()/window.clearTimeout() instead of the bare globals, so timers still fire correctly in Obsidian popout windows. connectionTestTimer is now typed as plain `number` (window.setTimeout's DOM return type) instead of ReturnType, which resolves to Node's Timeout under this repo's tsconfig due to @types/node's global augmentation colliding with the DOM lib. - tests/setup.ts: the node-env `window` polyfill only stubbed setInterval/clearInterval; add setTimeout/clearTimeout delegating to the real globals so vi.useFakeTimers() still controls them. --- src/services/github-service.ts | 4 ++-- src/settings.ts | 18 +++++++++--------- src/utils/git-blob-sha.ts | 2 +- tests/setup.ts | 5 +++++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/services/github-service.ts b/src/services/github-service.ts index dbce51d..aca736f 100644 --- a/src/services/github-service.ts +++ b/src/services/github-service.ts @@ -148,7 +148,7 @@ export class GitHubService extends BaseGitService implements GitServiceInterface const errorMessage = e instanceof Error ? e.message : String(e); const looksStale = /does not exist in tree|does not match|expectedHeadOid/i.test(errorMessage); if (!looksStale || attempt === maxAttempts) throw e; - await new Promise(resolve => setTimeout(resolve, 500 * attempt)); + await new Promise(resolve => window.setTimeout(resolve, 500 * attempt)); } } // Unreachable: the loop always returns or throws. @@ -178,7 +178,7 @@ export class GitHubService extends BaseGitService implements GitServiceInterface const shaByPath = new Map(freshTree.map(e => [e.path, e.sha])); const results = items.map((item, i) => ({ path: item.path, sha: shaByPath.get(fullPaths[i] as string) })); if (results.every(r => r.sha) || attempt === 3) return results; - await new Promise(resolve => setTimeout(resolve, 500 * attempt)); + await new Promise(resolve => window.setTimeout(resolve, 500 * attempt)); } // Unreachable: the loop always returns on its last iteration. throw new Error('pushBatch: exhausted retries reading back blob shas'); diff --git a/src/settings.ts b/src/settings.ts index 1d4370a..de704d4 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -105,7 +105,7 @@ const CONNECTION_TEST_DEBOUNCE_MS = 800; export class GitLabSyncSettingTab extends PluginSettingTab { plugin: GitLabFilesPush; private statusBadgeEl: HTMLElement | null = null; - private connectionTestTimer: ReturnType | null = null; + private connectionTestTimer: number | null = null; private unsubscribeConnectionStatus: (() => void) | null = null; constructor(app: App, plugin: GitLabFilesPush) { @@ -120,7 +120,7 @@ export class GitLabSyncSettingTab extends PluginSettingTab { this.unsubscribeConnectionStatus?.(); this.unsubscribeConnectionStatus = null; if (this.connectionTestTimer) { - clearTimeout(this.connectionTestTimer); + window.clearTimeout(this.connectionTestTimer); this.connectionTestTimer = null; } } @@ -153,11 +153,6 @@ export class GitLabSyncSettingTab extends PluginSettingTab { } } - // Rebuilding the whole settings tab (renderSettings) to refresh the badge - // would empty and recreate every field, stealing focus mid-typing. The - // badge element is instead created once per renderSettings pass and - // updated in place by setStatusBadge(), driven by the plugin's shared - // connection status (see main.ts) so it stays in sync with the status bar. // Persistent (until dismissed) banner surfacing the current version's notable // highlights right at the top of the settings tab, so users who dismissed or // never saw the WhatsNewModal (see main.ts) can still find them. Separate @@ -193,6 +188,11 @@ export class GitLabSyncSettingTab extends PluginSettingTab { }); } + // Rebuilding the whole settings tab (renderSettings) to refresh the badge + // would empty and recreate every field, stealing focus mid-typing. The + // badge element is instead created once per renderSettings pass and + // updated in place by setStatusBadge(), driven by the plugin's shared + // connection status (see main.ts) so it stays in sync with the status bar. private renderConnectionStatus(containerEl: HTMLElement): void { this.statusBadgeEl = containerEl.createDiv({ cls: 'gfs-connection-status' }); this.unsubscribeConnectionStatus?.(); @@ -219,9 +219,9 @@ export class GitLabSyncSettingTab extends PluginSettingTab { // don't hit the remote API on every character typed. private scheduleConnectionTest(): void { if (this.connectionTestTimer) { - clearTimeout(this.connectionTestTimer); + window.clearTimeout(this.connectionTestTimer); } - this.connectionTestTimer = setTimeout(() => { + this.connectionTestTimer = window.setTimeout(() => { this.connectionTestTimer = null; void this.plugin.testConnection(); }, CONNECTION_TEST_DEBOUNCE_MS); diff --git a/src/utils/git-blob-sha.ts b/src/utils/git-blob-sha.ts index 6934d9d..0a1dc72 100644 --- a/src/utils/git-blob-sha.ts +++ b/src/utils/git-blob-sha.ts @@ -15,7 +15,7 @@ export async function gitBlobSha(content: string | ArrayBuffer): Promise // SHA-1 isn't for security here — it's required because it's git's own // object-hashing algorithm, so this must match `git hash-object` exactly. - // eslint-disable-next-line sonarjs/hashing + // eslint-disable-next-line sonarjs/hashing -- SHA-1 is required here to match git's own object-hashing algorithm, not used for security const digest = await crypto.subtle.digest('SHA-1', combined); return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join(''); } diff --git a/tests/setup.ts b/tests/setup.ts index b264080..fc0c872 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -21,6 +21,11 @@ if (typeof window === 'undefined') { (globalThis as unknown as { window: unknown }).window = { setInterval: vi.fn(), clearInterval: vi.fn(), + // Delegate to the global timers (not bound methods captured once) so + // vi.useFakeTimers()/vi.useRealTimers() — which patch globalThis — keep + // controlling window.setTimeout/clearTimeout too. + setTimeout: (handler: (...args: unknown[]) => void, timeout?: number) => globalThis.setTimeout(handler, timeout), + clearTimeout: (handle?: ReturnType) => globalThis.clearTimeout(handle), }; }