Files
jellytau/src/lib/services/syncService.ts
T
dtourolle d54d8cc7c4 refactor(logging): route frontend console calls through the logger
TRACES: | DR-204

484 ungated `console.*` calls across 63 non-test frontend files shipped to
end users with no way to turn them off. Mechanical substitution, no control
flow, error handling or message semantics changed:

  console.log / console.debug -> log.debug
  console.info                -> log.info
  console.warn                -> log.warn
  console.error               -> log.error

Hand-written `"[Scope] …"` prefixes are dropped where the logger's scope
now carries them; scope names that already existed are preserved verbatim
(`[Auth]`, `[VideoPlayer]`, `[PiP]`, …) and inferred from the filename
where a file had none. `src/routes/player/[id]/+page.svelte` keeps its
`NextEpisode` and `AutoPlay` sub-scopes as separate loggers rather than
flattening them into the page scope.

`grep -rn 'console\.' src/` now matches nothing outside the tests and the
facade itself.
2026-08-20 19:29:59 +02:00

207 lines
6.2 KiB
TypeScript

// Sync service - manages offline mutation queueing
//
// Simplified service that coordinates with the Rust backend.
// The Rust backend handles sync queue persistence and processing logic.
// This service provides a thin TypeScript API for queuing mutations.
//
// TRACES: UR-002, UR-017, UR-025 | DR-014
import { commands } from "$lib/api/bindings";
import { auth } from "$lib/stores/auth";
// The queue row shape comes from the generated bindings rather than a
// hand-written mirror — the mirror had already drifted (it predates `itemName`),
// and a drifted duplicate is how a field silently stops reaching the UI.
import type { SyncQueueItem } from "$lib/api/bindings";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("SyncService");
export type { SyncQueueItem };
export type SyncOperation =
| "mark_played"
| "mark_unplayed"
| "mark_favorite"
| "unmark_favorite"
| "update_progress"
| "report_playback_start"
| "report_playback_stopped"
| "playlist_create"
| "playlist_delete"
| "playlist_rename"
| "playlist_add_items"
| "playlist_remove_items"
| "playlist_reorder_item";
/**
* Simplified sync service - handles offline mutation queueing
*
* The Rust backend maintains the sync queue in SQLite and is responsible
* for processing queued items. This service provides a TypeScript API
* for queueing and managing sync operations.
*/
class SyncService {
/**
* Start the sync service (lifecycle managed by Rust backend)
*/
start(): void {
log.debug("Started");
}
/**
* Stop the sync service (lifecycle managed by Rust backend)
*/
stop(): void {
log.debug("Stopped");
}
/**
* Queue a mutation for sync to server
*
* TRACES: UR-017, UR-025 | DR-014
*/
async queueMutation(
operation: SyncOperation,
itemId: string,
payload?: Record<string, unknown>
): Promise<number> {
const userId = auth.getUserId();
if (!userId) {
throw new Error("Not authenticated");
}
const id = await commands.syncQueueMutation(
userId,
operation,
itemId,
payload ? JSON.stringify(payload) : null
);
log.debug(`Queued ${operation} for item ${itemId}, id: ${id}`);
return id;
}
// NOTE: `queueFavorite` is gone. Favourites are drained by Rust on the
// `connectivity:reconnected` signal (DR-120) — the local write already sets
// `pending_sync`, and a second queue here would push the same change twice.
// See src-tauri/src/commands/favorites.rs.
/**
* Queue playback progress update
* Also updates local state immediately
*/
async queuePlaybackProgress(
itemId: string,
positionMs: number
): Promise<number> {
// Update local state first
await commands.storageUpdatePlaybackProgress(auth.getUserId() ?? "", itemId, positionMs);
return this.queueMutation("update_progress", itemId, { positionMs });
}
/**
* Queue mark as played
* Also updates local state immediately
*/
async queueMarkPlayed(itemId: string): Promise<number> {
// storageSetWatched, not storageMarkPlayed: this is the watched *toggle*, so
// it has to cover a season or series' episodes too. storageMarkPlayed stays
// the single-item "this finished playing" path.
await commands.storageSetWatched(auth.getUserId() ?? "", itemId, true);
return this.queueMutation("mark_played", itemId);
}
/**
* Queue mark as unwatched, the inverse of {@link queueMarkPlayed}.
*
* Same shape deliberately: the watched toggle has to work in both directions
* offline, or un-marking would be the one half that needs a connection. The
* drain pushes this as `clear_watch_history` — Jellyfin's mark-unplayed, which
* is recursive over a season or series and also clears resume positions.
*
* TRACES: UR-073 | DR-158
*/
async queueMarkUnplayed(itemId: string): Promise<number> {
await commands.storageSetWatched(auth.getUserId() ?? "", itemId, false);
return this.queueMutation("mark_unplayed", itemId);
}
/**
* Get count of pending sync operations
*/
async getPendingCount(): Promise<number> {
const userId = auth.getUserId();
if (!userId) {
return 0;
}
return commands.syncGetPendingCount(userId);
}
/**
* Get pending sync items (for debugging/monitoring)
*/
async getPending(limit?: number): Promise<SyncQueueItem[]> {
const userId = auth.getUserId();
if (!userId) {
return [];
}
return commands.syncGetPending(userId, limit ?? null);
}
/**
* Clean up completed operations older than specified days
*/
async cleanup(daysOld: number = 7): Promise<number> {
const deleted = await commands.syncCleanupCompleted(daysOld);
log.debug(`Cleaned up ${deleted} old completed operations`);
return deleted;
}
// ===== Playlist sync operations =====
async queuePlaylistCreate(playlistId: string, name: string, itemIds: string[]): Promise<number> {
return this.queueMutation("playlist_create", playlistId, { name, itemIds });
}
async queuePlaylistDelete(playlistId: string): Promise<number> {
return this.queueMutation("playlist_delete", playlistId);
}
async queuePlaylistRename(playlistId: string, name: string): Promise<number> {
return this.queueMutation("playlist_rename", playlistId, { name });
}
async queuePlaylistAddItems(playlistId: string, itemIds: string[]): Promise<number> {
return this.queueMutation("playlist_add_items", playlistId, { itemIds });
}
async queuePlaylistRemoveItems(playlistId: string, entryIds: string[]): Promise<number> {
return this.queueMutation("playlist_remove_items", playlistId, { entryIds });
}
async queuePlaylistReorderItem(playlistId: string, itemId: string, newIndex: number): Promise<number> {
return this.queueMutation("playlist_reorder_item", playlistId, { itemId, newIndex });
}
/**
* Clear all sync operations for the current user (called during logout)
*
* TRACES: UR-017 | DR-014
*/
async clearUser(): Promise<void> {
const userId = auth.getUserId();
if (userId) {
await commands.syncClearUser(userId);
log.debug("Cleared sync queue for user");
}
}
}
// Export singleton instance
export const syncService = new SyncService();