import { randomBytes } from 'node:crypto'; import type { JoplinAdapter, NoteQuery } from '../src/adapter/joplin-adapter.js'; import type { ChangePage, Note, Notebook, Pagination, RevisionRecord, Tag } from '../src/domain/types.js'; const id = () => randomBytes(16).toString('hex'); export class FakeAdapter implements JoplinAdapter { public notebooks: Notebook[] = []; public notes: Note[] = []; public tags: Tag[] = []; public revisions: RevisionRecord[] = []; public syncCalls = 0; public dataCalls = 0; public failSyncCalls = new Set(); public delayMs = 0; public concurrentCalls = 0; public maxConcurrentCalls = 0; public async prepare(): Promise {} public async close(): Promise {} public async sync(): Promise { await this.track(async () => { this.syncCalls += 1; if (this.failSyncCalls.has(this.syncCalls)) throw new Error('sync offline'); }); } public async listNotebooks(): Promise { return this.data(() => structuredClone(this.notebooks)); } public async getNotebook(notebookId: string): Promise { return this.data(() => structuredClone(this.notebooks.find(item => item.id === notebookId) ?? null)); } public async createNotebook(input: { title: string; parent_id?: string }): Promise { return this.data(() => { const notebook: Notebook = { id: id(), parent_id: input.parent_id ?? '', title: input.title, created_time: Date.now(), updated_time: Date.now() }; this.notebooks.push(notebook); return structuredClone(notebook); }); } public async listNotes(query: NoteQuery): Promise { return this.data(() => structuredClone(this.notes.filter(note => (query.parent_id === undefined || note.parent_id === query.parent_id) && (query.include_deleted || !note.deleted_time)))); } public async getNote(noteId: string, includeDeleted = false): Promise { return this.data(() => { const note = this.notes.find(item => item.id === noteId && (includeDeleted || !item.deleted_time)); return structuredClone(note ?? null); }); } public async createNote(input: { parent_id: string; title: string; body: string }): Promise { return this.data(() => { const now = Date.now(); const note: Note = { id: id(), ...input, created_time: now, updated_time: now, deleted_time: 0, is_todo: 0, is_conflict: 0 }; this.notes.push(note); return structuredClone(note); }); } public async updateNote(noteId: string, input: Partial>): Promise { return this.data(() => { const note = this.notes.find(item => item.id === noteId); if (!note) throw new Error('not found'); Object.assign(note, input, { updated_time: Date.now() }); return structuredClone(note); }); } public async deleteNote(noteId: string): Promise { await this.data(() => { const note = this.notes.find(item => item.id === noteId); if (note) note.deleted_time = Date.now(); }); } public async listTags(): Promise { return this.data(() => structuredClone(this.tags)); } public async getTag(tagId: string): Promise { return this.data(() => structuredClone(this.tags.find(tag => tag.id === tagId) ?? null)); } public async createTag(input: { title: string }): Promise { return this.data(() => { const tag = { id: id(), title: input.title }; this.tags.push(tag); return structuredClone(tag); }); } public async listNoteTags(): Promise { return this.data(() => []); } public async listTagNotes(): Promise { return this.data(() => []); } public async addTagToNote(): Promise { await this.data(() => undefined); } public async removeTagFromNote(): Promise { await this.data(() => undefined); } public async search(_query: string, _pagination: Pagination): Promise { return this.data(() => structuredClone(this.notes)); } public async listRevisions(noteId: string): Promise { return this.data(() => structuredClone(this.revisions.filter(revision => revision.item_id === noteId))); } public async listChanges(cursor: string | undefined): Promise { return this.data(() => ({ items: [], cursor: cursor ?? '0', has_more: false })); } private async data(operation: () => T): Promise { this.dataCalls += 1; return this.track(operation); } private async track(operation: () => T): Promise { this.concurrentCalls += 1; this.maxConcurrentCalls = Math.max(this.maxConcurrentCalls, this.concurrentCalls); try { if (this.delayMs) await new Promise(resolve => setTimeout(resolve, this.delayMs)); return operation(); } finally { this.concurrentCalls -= 1; } } }