From a29c113b5f23b556b186178441890514aa9fc359 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:30:26 +0200 Subject: [PATCH] Migrate manga only after "copy" succeeded In case the manga should not only be copied but actually migrated, the "migration" actions should only be executed in case the copy actions have succeeded. --- src/UtilTypes.d.ts | 4 ++ src/lib/data/Mangas.ts | 152 +++++++++++++++++++++++++++-------------- 2 files changed, 106 insertions(+), 50 deletions(-) diff --git a/src/UtilTypes.d.ts b/src/UtilTypes.d.ts index 82c297b4..72f5b2d5 100644 --- a/src/UtilTypes.d.ts +++ b/src/UtilTypes.d.ts @@ -25,3 +25,7 @@ type OptionalProperty = Omit & Partial>; type PropertiesNever = { [key in keyof T]?: never }; type OmitFirst = T extends [any, ...infer R] ? R : never; + +type TupleUnion = { + [S in U]: Exclude extends never ? [...R, S] : TupleUnion, [...R, S]>; +}[U]; diff --git a/src/lib/data/Mangas.ts b/src/lib/data/Mangas.ts index f9e4ec30..d4d66f3a 100644 --- a/src/lib/data/Mangas.ts +++ b/src/lib/data/Mangas.ts @@ -174,6 +174,8 @@ type PerformActionOptions = Action extends 'mark_as_ ? DownloadActionOption : DefaultActionOption; +type MigrateFuncReturn = { copy: () => Promise[]; cleanup: () => Promise[] }; + export class Mangas { static getIds(mangas: MangaIdInfo[]): number[] { return mangas.map((manga) => manga.id); @@ -364,10 +366,11 @@ export class Mangas { ); } - private static async migrateChapters( + private static migrateChapters( + mode: MigrateMode, mangaToMigrate: GetMangaToMigrateQuery['manga'], mangaToMigrateToInfo: GetMangaToMigrateToFetchMutation, - ): Promise { + ): MigrateFuncReturn { if (!mangaToMigrate.chapters || !mangaToMigrateToInfo.fetchChapters?.chapters) { throw new Error('Chapters are missing'); } @@ -392,31 +395,48 @@ export class Mangas { } }); - await Promise.all([ - readChapters.length && requestManager.updateChapters(readChapters, { isRead: true }).response, - bookmarkedChapters.length && - requestManager.updateChapters(bookmarkedChapters, { isBookmarked: true }).response, - ]); + return { + copy: () => + [ + readChapters.length && requestManager.updateChapters(readChapters, { isRead: true }).response, + bookmarkedChapters.length && + requestManager.updateChapters(bookmarkedChapters, { isBookmarked: true }).response, + ].filter((promise) => !!promise), + cleanup: () => + mode === 'migrate' + ? [ + requestManager.deleteDownloadedChapters( + Chapters.getIds(Chapters.getDownloaded(mangaToMigrate.chapters?.nodes ?? [])), + ).response, + ] + : [], + }; } - private static async migrateCategories( + private static migrateCategories( + mode: MigrateMode, mangaToMigrate: MangaToMigrate, mangaToMigrateTo: MangaToMigrateTo, - ): Promise { + ): MigrateFuncReturn { if (!mangaToMigrate?.categories) { throw new Error('Categories are missing'); } - await requestManager.updateMangasCategories([mangaToMigrateTo.id], { - addToCategories: mangaToMigrate.categories.nodes.map((category) => category.id), - }).response; + return { + copy: () => [ + requestManager.updateMangasCategories([mangaToMigrateTo.id], { + addToCategories: mangaToMigrate.categories?.nodes.map((category) => category.id), + }).response, + ], + cleanup: () => [], + }; } - private static async migrateTracking( + private static migrateTracking( mode: MigrateMode, mangaToMigrate: MangaToMigrate, mangaToMigrateTo: MangaToMigrateTo, - ): Promise { + ): MigrateFuncReturn { if (!mangaToMigrate.trackRecords) { throw new Error('TrackRecords of manga to migrate are missing'); } @@ -426,23 +446,44 @@ export class Mangas { } const trackBindingsToAdd = mangaToMigrate.trackRecords.nodes.filter((trackRecordToMigrate) => - mangaToMigrateTo.trackRecords!.nodes.every( + mangaToMigrateTo.trackRecords?.nodes.every( (trackRecord) => trackRecordToMigrate.remoteId !== trackRecord.remoteId, ), ); - await Promise.all([ - ...(mode === 'migrate' - ? mangaToMigrate.trackRecords.nodes.map( - (trackRecord) => requestManager.unbindTracker(trackRecord.id).response, - ) - : []), - ...trackBindingsToAdd.map( - (trackRecord) => - requestManager.bindTracker(mangaToMigrateTo.id, trackRecord.trackerId, trackRecord.remoteId) - .response, - ), - ]); + return { + copy: () => + trackBindingsToAdd.map( + (trackRecord) => + requestManager.bindTracker(mangaToMigrateTo.id, trackRecord.trackerId, trackRecord.remoteId) + .response, + ), + cleanup: () => + mode === 'migrate' + ? (mangaToMigrate.trackRecords?.nodes.map( + (trackRecord) => requestManager.unbindTracker(trackRecord.id).response, + ) ?? []) + : [], + }; + } + + private static migrateManga( + mode: MigrateMode, + removeMangaFromCategories: boolean, + mangaToMigrate: MangaToMigrate, + ): MigrateFuncReturn { + return { + copy: () => [], + cleanup: () => + mode === 'migrate' + ? [ + requestManager.updateManga(mangaToMigrate.id, { + updateManga: { inLibrary: false }, + updateMangaCategories: removeMangaFromCategories ? { clearCategories: true } : undefined, + }).response, + ] + : [], + }; } static async migrate( @@ -484,29 +525,40 @@ export class Mangas { throw new Error('Mangas::migrate: missing chapters data'); } - await Promise.all([ - migrateChapters ? Mangas.migrateChapters(mangaToMigrateData.manga, mangaToMigrateToData) : undefined, - deleteChapters - ? requestManager.deleteDownloadedChapters( - Chapters.getIds(Chapters.getDownloaded(mangaToMigrateData.manga.chapters?.nodes ?? [])), - ).response - : undefined, - migrateCategories - ? Mangas.migrateCategories(mangaToMigrateData.manga, mangaToMigrateToData.fetchManga.manga) - : undefined, - migrateTracking - ? Mangas.migrateTracking(mode, mangaToMigrateData.manga, mangaToMigrateToData.fetchManga.manga) - : undefined, - !mangaToMigrateToData.fetchManga.manga.inLibrary - ? requestManager.updateManga(mangaIdToMigrateTo, { updateManga: { inLibrary: true } }).response - : undefined, - mode === 'migrate' - ? requestManager.updateManga(mangaId, { - updateManga: { inLibrary: false }, - updateMangaCategories: removeMangaFromCategories ? { clearCategories: true } : undefined, - }).response - : undefined, - ]); + const performMigrationAction = async ( + migrateAction: keyof MigrateFuncReturn, + ...actions: [boolean | undefined, MigrateFuncReturn][] + ) => + Promise.all( + actions + .filter(([performAction]) => performAction) + .map(([, action]) => action[migrateAction]()) + .flat(), + ); + + const performMigrationActions = async (...actions: [boolean | undefined, MigrateFuncReturn][]) => { + const migrationActions: TupleUnion = ['copy', 'cleanup']; + + for (const migrationAction of migrationActions) { + // the migration actions (copy, cleanup) are supposed to be run sequentially to ensure that the cleanup + // only happens in case the copy succeeded + // eslint-disable-next-line no-await-in-loop + await performMigrationAction(migrationAction, ...actions); + } + }; + + await performMigrationActions( + [migrateChapters, Mangas.migrateChapters(mode, mangaToMigrateData.manga, mangaToMigrateToData)], + [ + migrateCategories, + Mangas.migrateCategories(mode, mangaToMigrateData.manga, mangaToMigrateToData.fetchManga.manga), + ], + [ + migrateTracking, + Mangas.migrateTracking(mode, mangaToMigrateData.manga, mangaToMigrateToData.fetchManga.manga), + ], + [true, Mangas.migrateManga(mode, removeMangaFromCategories, mangaToMigrateData.manga)], + ); }); }