227 lines
6.1 KiB
TypeScript
227 lines
6.1 KiB
TypeScript
import * as cron from "node-cron";
|
|
import {
|
|
createNote,
|
|
getNotes,
|
|
getNoteById,
|
|
updateNote,
|
|
deleteNote,
|
|
saveYjsUpdate as saveYjsUpdateToDB,
|
|
loadYjsUpdates as loadYjsUpdatesFromDB,
|
|
getUniqueNoteIds,
|
|
getYjsUpdatesByNoteId,
|
|
replaceYjsUpdates,
|
|
} from "../db/notes";
|
|
import * as Y from "yjs";
|
|
import { ipcMain } from "electron";
|
|
import { logger } from "../main/logger";
|
|
|
|
export interface NoteCreateOptions {
|
|
title: string;
|
|
initialContent?: string;
|
|
icon?: string | null;
|
|
}
|
|
|
|
export interface NoteUpdateOptions {
|
|
title?: string;
|
|
transcriptionId?: number | null;
|
|
icon?: string | null;
|
|
}
|
|
|
|
class NotesService {
|
|
private static instance: NotesService;
|
|
private compactionTask: cron.ScheduledTask | null = null;
|
|
|
|
private constructor() {
|
|
this.setupIPCHandlers();
|
|
this.setupCompactionCron();
|
|
}
|
|
|
|
private setupIPCHandlers(): void {
|
|
ipcMain.handle(
|
|
"notes:saveYjsUpdate",
|
|
async (_event, noteId: number, update: ArrayBuffer) => {
|
|
try {
|
|
const updateArray = new Uint8Array(update);
|
|
await this.saveYjsUpdate(noteId, updateArray);
|
|
logger.main.debug("Saved yjs update", {
|
|
noteId,
|
|
updateSize: updateArray.length,
|
|
});
|
|
} catch (error) {
|
|
logger.main.error("Failed to save yjs update", error);
|
|
throw error;
|
|
}
|
|
},
|
|
);
|
|
|
|
ipcMain.handle("notes:loadYjsUpdates", async (_event, noteId: number) => {
|
|
try {
|
|
const updates = await this.loadYjsUpdates(noteId);
|
|
logger.main.debug("Loaded yjs updates", {
|
|
noteId,
|
|
count: updates.length,
|
|
});
|
|
return updates.map((u) => u.buffer);
|
|
} catch (error) {
|
|
logger.main.error("Failed to load yjs updates", error);
|
|
throw error;
|
|
}
|
|
});
|
|
}
|
|
|
|
public static getInstance(): NotesService {
|
|
if (!NotesService.instance) {
|
|
NotesService.instance = new NotesService();
|
|
}
|
|
return NotesService.instance;
|
|
}
|
|
|
|
async createNote(options: NoteCreateOptions) {
|
|
// Create the note in the database
|
|
const note = await createNote({
|
|
title: options.title,
|
|
icon: options.icon,
|
|
});
|
|
|
|
// Initialize yjs document with initial content if provided
|
|
if (options.initialContent) {
|
|
const ydoc = new Y.Doc();
|
|
const text = ydoc.getText("content");
|
|
text.insert(0, options.initialContent);
|
|
|
|
// Save initial content as a YJS update
|
|
const initialUpdate = Y.encodeStateAsUpdate(ydoc);
|
|
await saveYjsUpdateToDB(note.id, initialUpdate);
|
|
}
|
|
|
|
return note;
|
|
}
|
|
|
|
async getNote(id: number) {
|
|
const note = await getNoteById(id);
|
|
return note;
|
|
}
|
|
|
|
async listNotes(options?: {
|
|
limit?: number;
|
|
offset?: number;
|
|
sortBy?: "title" | "updatedAt" | "createdAt";
|
|
sortOrder?: "asc" | "desc";
|
|
search?: string;
|
|
transcriptionId?: number | null;
|
|
}) {
|
|
return await getNotes(options);
|
|
}
|
|
|
|
async updateNote(id: number, options: NoteUpdateOptions) {
|
|
return await updateNote(id, options);
|
|
}
|
|
|
|
async deleteNote(id: number) {
|
|
const note = await getNoteById(id);
|
|
if (!note) return null;
|
|
|
|
return await deleteNote(id);
|
|
}
|
|
|
|
// Save yjs update to database
|
|
async saveYjsUpdate(noteId: number, update: Uint8Array) {
|
|
await saveYjsUpdateToDB(noteId, update);
|
|
}
|
|
|
|
// Load all yjs updates for a note
|
|
async loadYjsUpdates(noteId: number): Promise<Uint8Array[]> {
|
|
return await loadYjsUpdatesFromDB(noteId);
|
|
}
|
|
|
|
// Compact all note documents
|
|
async compactAllNotes(): Promise<void> {
|
|
const startTime = Date.now();
|
|
logger.main.info("Starting yjs compaction for all notes");
|
|
|
|
try {
|
|
// Get all unique note IDs that have updates
|
|
const noteIds = await getUniqueNoteIds();
|
|
logger.main.info(`Found ${noteIds.length} notes to compact`);
|
|
|
|
let totalUpdatesBefore = 0;
|
|
let totalUpdatesAfter = 0;
|
|
|
|
for (const noteId of noteIds) {
|
|
const compactResult = await this.compactNote(noteId);
|
|
totalUpdatesBefore += compactResult.updatesBefore;
|
|
totalUpdatesAfter += compactResult.updatesAfter;
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.main.info(`Compaction completed in ${duration}ms`, {
|
|
notesCompacted: noteIds.length,
|
|
totalUpdatesBefore,
|
|
totalUpdatesAfter,
|
|
updatesReduced: totalUpdatesBefore - totalUpdatesAfter,
|
|
});
|
|
} catch (error) {
|
|
logger.main.error("Failed to compact notes:", error);
|
|
}
|
|
}
|
|
|
|
// Compact a specific note
|
|
async compactNote(
|
|
noteId: number,
|
|
): Promise<{ updatesBefore: number; updatesAfter: number }> {
|
|
// Get all updates for this note
|
|
const updates = await getYjsUpdatesByNoteId(noteId);
|
|
const updatesBefore = updates.length;
|
|
|
|
if (updatesBefore <= 1) {
|
|
// No need to compact if there's only one update or none
|
|
return { updatesBefore, updatesAfter: updatesBefore };
|
|
}
|
|
|
|
// Create a new Y.Doc and apply all updates
|
|
const ydoc = new Y.Doc();
|
|
for (const update of updates) {
|
|
const updateArray = new Uint8Array(update.updateData as Buffer);
|
|
Y.applyUpdate(ydoc, updateArray);
|
|
}
|
|
|
|
// Encode the current state as a single update
|
|
const stateUpdate = Y.encodeStateAsUpdate(ydoc);
|
|
|
|
// Replace all updates with the compacted one
|
|
await replaceYjsUpdates(noteId, stateUpdate);
|
|
|
|
logger.main.debug(
|
|
`Compacted note ${noteId}: ${updatesBefore} updates -> 1 update`,
|
|
);
|
|
|
|
return { updatesBefore, updatesAfter: 1 };
|
|
}
|
|
|
|
// Set up cron job for scheduled compaction
|
|
private setupCompactionCron() {
|
|
// Schedule for daily at 2 AM in production, every 5 minutes in development
|
|
const schedule =
|
|
process.env.NODE_ENV === "development" ? "*/5 * * * *" : "0 2 * * *";
|
|
|
|
this.compactionTask = cron.schedule(schedule, async () => {
|
|
logger.main.info(
|
|
`Running scheduled yjs compaction (schedule: ${schedule})`,
|
|
);
|
|
await this.compactAllNotes();
|
|
});
|
|
|
|
logger.main.info(`Yjs compaction cron job scheduled: ${schedule}`);
|
|
}
|
|
|
|
// Clean up resources
|
|
cleanup() {
|
|
// Stop the cron job
|
|
if (this.compactionTask) {
|
|
this.compactionTask.stop();
|
|
this.compactionTask = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export default NotesService;
|