feat(#89): Add backup functionality for google drive and dropbox

This commit is contained in:
Josh Creek
2026-01-24 13:32:10 +00:00
parent 3f5c14a8bc
commit ab82f75dc6
22 changed files with 1582 additions and 7 deletions
+17 -3
View File
@@ -14,6 +14,7 @@ type QueueItem = {
attempts: number;
notBefore: number; // unix ms
debounceTimer: ReturnType<typeof setTimeout> | null;
version: number;
};
export type CreateCatchRecordWriteQueueOptions = {
@@ -157,14 +158,25 @@ export function createCatchRecordWriteQueue(options: CreateCatchRecordWriteQueue
}
// Success: drop the flushed keys.
for (const [k] of eligible) {
items.delete(k);
for (const [k, sentItem] of eligible) {
const current = items.get(k);
if (!current) {
items.delete(k);
continue;
}
// Only clear if nothing newer was enqueued after this batch started.
if (current.version === sentItem.version) {
items.delete(k);
}
}
updateStatus({ lastError: null, lastSuccessfulFlushAt: Date.now() });
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
// Backoff all eligible items and keep them.
for (const [k, item] of eligible) {
const current = items.get(k);
if (!current) continue;
if (current.version !== item.version) continue;
const nextAttempts = item.attempts + 1;
items.set(k, {
...item,
@@ -199,6 +211,7 @@ export function createCatchRecordWriteQueue(options: CreateCatchRecordWriteQueue
const now = Date.now();
const existing = items.get(k);
const debounceMs = opts?.debounceMs ?? 0;
const nextVersion = (existing?.version ?? 0) + 1;
// Clear any pending debounce timer: latest state always wins.
if (existing?.debounceTimer) {
@@ -222,7 +235,8 @@ export function createCatchRecordWriteQueue(options: CreateCatchRecordWriteQueue
record,
attempts: existing?.attempts ?? 0,
notBefore,
debounceTimer
debounceTimer,
version: nextVersion
});
updateStatus({});
if (opts?.flushSoon !== false) scheduleFlush();
+3 -1
View File
@@ -1,3 +1,5 @@
import { env } from '$env/dynamic/private';
export function getEnv() {
return process.env;
return env;
}
+56
View File
@@ -0,0 +1,56 @@
import { randomUUID } from 'node:crypto';
import type { RequestEvent } from '@sveltejs/kit';
export type OAuthStatePayload = {
state: string;
userId: string;
pokedexId?: string | null;
provider: string;
fileName?: string | null;
folderId?: string | null;
path?: string | null;
returnTo?: string | null;
};
const STATE_MAX_AGE = 10 * 60; // 10 minutes
export function createOAuthState(): string {
return randomUUID();
}
function cookieName(provider: string): string {
return `oauth_state_${provider}`;
}
export function setOAuthStateCookie(
event: RequestEvent,
provider: string,
payload: OAuthStatePayload
) {
event.cookies.set(cookieName(provider), JSON.stringify(payload), {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: event.url.protocol === 'https:',
maxAge: STATE_MAX_AGE
});
}
export function readOAuthStateCookie(
event: RequestEvent,
provider: string
): OAuthStatePayload | null {
const raw = event.cookies.get(cookieName(provider));
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as OAuthStatePayload;
if (!parsed?.state || !parsed?.userId) return null;
return parsed;
} catch {
return null;
}
}
export function clearOAuthStateCookie(event: RequestEvent, provider: string) {
event.cookies.delete(cookieName(provider), { path: '/' });
}