mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 18:22:41 -08:00
120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
export interface PollingRunnerDeps {
|
|
getDeck: () => string | undefined;
|
|
getPollingRate: () => number;
|
|
findNotes: (
|
|
query: string,
|
|
options?: {
|
|
maxRetries?: number;
|
|
},
|
|
) => Promise<number[]>;
|
|
shouldAutoUpdateNewCards: () => boolean;
|
|
processNewCard: (noteId: number) => Promise<void>;
|
|
isUpdateInProgress: () => boolean;
|
|
setUpdateInProgress: (value: boolean) => void;
|
|
getTrackedNoteIds: () => Set<number>;
|
|
setTrackedNoteIds: (noteIds: Set<number>) => void;
|
|
showStatusNotification: (message: string) => void;
|
|
logDebug: (...args: unknown[]) => void;
|
|
logInfo: (...args: unknown[]) => void;
|
|
logWarn: (...args: unknown[]) => void;
|
|
}
|
|
|
|
export class PollingRunner {
|
|
private pollingInterval: ReturnType<typeof setInterval> | null = null;
|
|
private initialized = false;
|
|
private backoffMs = 200;
|
|
private maxBackoffMs = 5000;
|
|
private nextPollTime = 0;
|
|
|
|
constructor(private readonly deps: PollingRunnerDeps) {}
|
|
|
|
get isRunning(): boolean {
|
|
return this.pollingInterval !== null;
|
|
}
|
|
|
|
start(): void {
|
|
if (this.pollingInterval) {
|
|
this.stop();
|
|
}
|
|
|
|
void this.pollOnce();
|
|
this.pollingInterval = setInterval(() => {
|
|
void this.pollOnce();
|
|
}, this.deps.getPollingRate());
|
|
}
|
|
|
|
stop(): void {
|
|
if (this.pollingInterval) {
|
|
clearInterval(this.pollingInterval);
|
|
this.pollingInterval = null;
|
|
}
|
|
}
|
|
|
|
async pollOnce(): Promise<void> {
|
|
if (this.deps.isUpdateInProgress()) return;
|
|
if (Date.now() < this.nextPollTime) return;
|
|
|
|
this.deps.setUpdateInProgress(true);
|
|
try {
|
|
const query = this.deps.getDeck() ? `"deck:${this.deps.getDeck()}" added:1` : 'added:1';
|
|
const noteIds = await this.deps.findNotes(query, {
|
|
maxRetries: 0,
|
|
});
|
|
const currentNoteIds = new Set(noteIds);
|
|
|
|
const previousNoteIds = this.deps.getTrackedNoteIds();
|
|
if (!this.initialized) {
|
|
this.deps.setTrackedNoteIds(currentNoteIds);
|
|
this.initialized = true;
|
|
this.deps.logInfo(`AnkiConnect initialized with ${currentNoteIds.size} existing cards`);
|
|
this.backoffMs = 200;
|
|
return;
|
|
}
|
|
|
|
const newNoteIds = Array.from(currentNoteIds).filter((id) => !previousNoteIds.has(id));
|
|
|
|
if (newNoteIds.length > 0) {
|
|
this.deps.logInfo('Found new cards:', newNoteIds);
|
|
|
|
for (const noteId of newNoteIds) {
|
|
previousNoteIds.add(noteId);
|
|
}
|
|
this.deps.setTrackedNoteIds(previousNoteIds);
|
|
|
|
if (this.deps.shouldAutoUpdateNewCards()) {
|
|
for (const noteId of newNoteIds) {
|
|
await this.deps.processNewCard(noteId);
|
|
}
|
|
} else {
|
|
this.deps.logInfo(
|
|
'New card detected (auto-update disabled). Press Ctrl+V to update from clipboard.',
|
|
);
|
|
}
|
|
}
|
|
|
|
if (this.backoffMs > 200) {
|
|
this.deps.logInfo('AnkiConnect connection restored');
|
|
}
|
|
this.backoffMs = 200;
|
|
} catch (error) {
|
|
const wasBackingOff = this.backoffMs > 200;
|
|
this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
|
|
this.nextPollTime = Date.now() + this.backoffMs;
|
|
if (!wasBackingOff) {
|
|
this.deps.logWarn('AnkiConnect polling failed, backing off...');
|
|
this.deps.showStatusNotification('AnkiConnect: unable to connect');
|
|
}
|
|
this.deps.logWarn((error as Error).message);
|
|
} finally {
|
|
this.deps.setUpdateInProgress(false);
|
|
}
|
|
}
|
|
|
|
async poll(): Promise<void> {
|
|
if (this.pollingInterval) {
|
|
return;
|
|
}
|
|
return this.pollOnce();
|
|
}
|
|
}
|