Cleanup deprecated metadata after migration
- delete deprecated metadata - save applied migration id per metadata holder
This commit is contained in:
@@ -10,6 +10,55 @@ import { AppMetadataKeys, IMetadataMigration } from '@/modules/metadata/Metadata
|
||||
|
||||
export const APP_METADATA_KEY_PREFIX = 'webUI_';
|
||||
|
||||
const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
|
||||
staticNav: undefined,
|
||||
showPageNumber: undefined,
|
||||
loadNextOnEnding: undefined,
|
||||
skipDupChapters: undefined,
|
||||
fitPageToWindow: undefined,
|
||||
scalePage: undefined,
|
||||
readerType: undefined,
|
||||
offsetFirstPage: undefined,
|
||||
migration: undefined,
|
||||
deleteChaptersManuallyMarkedRead: undefined,
|
||||
deleteChaptersWhileReading: undefined,
|
||||
deleteChaptersWithBookmark: undefined,
|
||||
downloadAheadLimit: undefined,
|
||||
showAddToLibraryCategorySelectDialog: undefined,
|
||||
ignoreFilters: undefined,
|
||||
removeMangaFromCategories: undefined,
|
||||
showTabSize: undefined,
|
||||
devices: undefined,
|
||||
migrateChapters: undefined,
|
||||
migrateCategories: undefined,
|
||||
migrateTracking: undefined,
|
||||
deleteChapters: undefined,
|
||||
migrateSortSettings: undefined,
|
||||
hideLibraryEntries: undefined,
|
||||
updateProgressAfterReading: undefined,
|
||||
updateProgressManualMarkRead: undefined,
|
||||
webUIInformAvailableUpdate: undefined,
|
||||
serverInformAvailableUpdate: undefined,
|
||||
readerWidth: undefined,
|
||||
savedSearches: undefined,
|
||||
showContinueReadingButton: undefined,
|
||||
showDownloadBadge: undefined,
|
||||
showUnreadBadge: undefined,
|
||||
gridLayout: undefined,
|
||||
sortBy: undefined,
|
||||
sortDesc: undefined,
|
||||
hasDownloadedChapters: undefined,
|
||||
hasBookmarkedChapters: undefined,
|
||||
hasUnreadChapters: undefined,
|
||||
hasDuplicateChapters: undefined,
|
||||
hasTrackerBinding: undefined,
|
||||
hasStatus: undefined,
|
||||
customThemes: undefined,
|
||||
mangaThumbnailBackdrop: undefined,
|
||||
};
|
||||
|
||||
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
|
||||
|
||||
export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
|
||||
// downloads
|
||||
'deleteChaptersManuallyMarkedRead',
|
||||
|
||||
@@ -38,7 +38,12 @@ export type MetadataHolder<Keys extends string = string, Values = string> = {
|
||||
|
||||
export type AllowedMetadataValueTypes = string | boolean | number | undefined | null;
|
||||
|
||||
interface MetadataAppliedMigration {
|
||||
migration: number;
|
||||
}
|
||||
|
||||
export type AppMetadataKeys =
|
||||
| keyof MetadataAppliedMigration
|
||||
| MetadataServerSettingKeys
|
||||
| MangaMetadataKeys
|
||||
| SearchMetadataKeys
|
||||
@@ -46,3 +51,5 @@ export type AppMetadataKeys =
|
||||
| CategoryMetadataKeys;
|
||||
|
||||
export type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes];
|
||||
|
||||
export type MetadataHolderType = 'manga' | 'chapter' | 'category' | 'global' | 'source';
|
||||
|
||||
33
src/modules/metadata/services/MetadataApolloCacheHandler.ts
Normal file
33
src/modules/metadata/services/MetadataApolloCacheHandler.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { ReadFieldFunction } from '@apollo/client/cache/core/types/common';
|
||||
import { Reference } from '@apollo/client/utilities';
|
||||
|
||||
export const updateMetadataList = (
|
||||
key: string,
|
||||
existingMetas: Reference[] | undefined,
|
||||
readField: ReadFieldFunction,
|
||||
createMetaRef: () => Reference | undefined,
|
||||
deleted: boolean = false,
|
||||
): (Reference | undefined)[] | undefined => {
|
||||
if (!existingMetas) {
|
||||
return existingMetas;
|
||||
}
|
||||
|
||||
if (deleted) {
|
||||
return existingMetas.filter((metaRef: Reference) => readField('key', metaRef) !== key);
|
||||
}
|
||||
|
||||
const exists = existingMetas.some((metaRef: Reference) => readField('key', metaRef) === key);
|
||||
if (exists) {
|
||||
return existingMetas;
|
||||
}
|
||||
|
||||
return [...existingMetas, createMetaRef()];
|
||||
};
|
||||
@@ -111,7 +111,8 @@ export const applyMetadataMigrations = (meta?: Metadata): Metadata | undefined =
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const migrationToMetadata: [number, Metadata][] = [[0, meta]];
|
||||
const appliedMigrationId = Number.isNaN(Number(meta.migration)) ? 0 : Math.max(0, Number(meta.migration) - 1);
|
||||
const migrationToMetadata: [number, Metadata][] = [[appliedMigrationId, meta]];
|
||||
|
||||
METADATA_MIGRATIONS.forEach((migration, index) => {
|
||||
const migrationId = index + 1;
|
||||
|
||||
@@ -6,16 +6,43 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { APP_METADATA_KEY_PREFIX, GLOBAL_METADATA_KEYS } from '@/modules/metadata/Metadata.constants.ts';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
APP_METADATA_KEY_PREFIX,
|
||||
GLOBAL_METADATA_KEYS,
|
||||
METADATA_MIGRATIONS,
|
||||
VALID_APP_METADATA_KEYS,
|
||||
} from '@/modules/metadata/Metadata.constants.ts';
|
||||
import { DEFAULT_DEVICE, getActiveDevice } from '@/modules/device/services/Device.ts';
|
||||
import { applyMetadataMigrations } from '@/modules/metadata/services/MetadataMigrations.ts';
|
||||
import { convertValueFromMetadata } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||
import { convertToGqlMeta, convertValueFromMetadata } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||
import {
|
||||
AllowedMetadataValueTypes,
|
||||
AppMetadataKeys,
|
||||
Metadata,
|
||||
MetadataHolder,
|
||||
MetadataHolderType,
|
||||
MetadataKeyValuePair,
|
||||
} from '@/modules/metadata/Metadata.types.ts';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
import {
|
||||
requestDeleteCategoryMetadata,
|
||||
requestDeleteChapterMetadata,
|
||||
requestDeleteMangaMetadata,
|
||||
requestDeleteServerMetadata,
|
||||
requestDeleteSourceMetadata,
|
||||
requestUpdateCategoryMetadata,
|
||||
requestUpdateChapterMetadata,
|
||||
requestUpdateMangaMetadata,
|
||||
requestUpdateServerMetadata,
|
||||
requestUpdateSourceMetadata,
|
||||
} from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
|
||||
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
|
||||
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
export const extractOriginalKey = (key: string): string => key.split('_').slice(-1)[0];
|
||||
|
||||
export const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => {
|
||||
const isGlobalMetadataKey = GLOBAL_METADATA_KEYS.includes(key as AppMetadataKeys);
|
||||
@@ -33,7 +60,9 @@ export const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends
|
||||
defaultValue?: Value,
|
||||
applyMigrations: boolean = true,
|
||||
): Value | undefined => {
|
||||
const metadata = applyMigrations ? applyMetadataMigrations(meta) : meta;
|
||||
const requiresMigration = Number(meta?.migration) !== METADATA_MIGRATIONS.length;
|
||||
const doMigration = requiresMigration && applyMigrations;
|
||||
const metadata = doMigration ? applyMetadataMigrations(meta) : meta;
|
||||
|
||||
if (
|
||||
metadata === undefined ||
|
||||
@@ -46,24 +75,187 @@ export const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends
|
||||
return convertValueFromMetadata(metadata[getMetadataKey(key)]);
|
||||
};
|
||||
|
||||
export const getMetadataFrom = <METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
{ meta }: MetadataHolder,
|
||||
const getMetadataUpdateFunction = (
|
||||
type: MetadataHolderType,
|
||||
metadataHolder:
|
||||
| MetadataHolder
|
||||
| (MangaIdInfo & MetadataHolder)
|
||||
| (ChapterIdInfo & MetadataHolder)
|
||||
| (CategoryIdInfo & MetadataHolder)
|
||||
| (Pick<SourceType, 'id'> & MetadataHolder),
|
||||
): ((appliedMigration: [MetadataKeyValuePair]) => Promise<void[]>) => {
|
||||
switch (type) {
|
||||
case 'global':
|
||||
return (appliedMigration) => requestUpdateServerMetadata(appliedMigration);
|
||||
case 'manga':
|
||||
return (appliedMigration) =>
|
||||
requestUpdateMangaMetadata(
|
||||
{ id: (metadataHolder as MangaIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'chapter':
|
||||
return (appliedMigration) =>
|
||||
requestUpdateChapterMetadata(
|
||||
{ id: (metadataHolder as ChapterIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'category':
|
||||
return (appliedMigration) =>
|
||||
requestUpdateCategoryMetadata(
|
||||
{ id: (metadataHolder as CategoryIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'source':
|
||||
return (appliedMigration) =>
|
||||
requestUpdateSourceMetadata(
|
||||
{
|
||||
id: (metadataHolder as Pick<SourceType, 'id'>).id,
|
||||
meta: convertToGqlMeta(metadataHolder.meta),
|
||||
},
|
||||
appliedMigration,
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unexpected "type" (${type})`);
|
||||
}
|
||||
};
|
||||
|
||||
const getMetadataDeleteFunction = (
|
||||
type: MetadataHolderType,
|
||||
metadataHolder:
|
||||
| MetadataHolder
|
||||
| (MangaIdInfo & MetadataHolder)
|
||||
| (ChapterIdInfo & MetadataHolder)
|
||||
| (CategoryIdInfo & MetadataHolder)
|
||||
| (Pick<SourceType, 'id'> & MetadataHolder),
|
||||
): ((metadataToDelete: AppMetadataKeys[]) => Promise<void[]>) => {
|
||||
switch (type) {
|
||||
case 'global':
|
||||
return (appliedMigration) => requestDeleteServerMetadata(appliedMigration);
|
||||
case 'manga':
|
||||
return (appliedMigration) =>
|
||||
requestDeleteMangaMetadata(
|
||||
{ id: (metadataHolder as MangaIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'chapter':
|
||||
return (appliedMigration) =>
|
||||
requestDeleteChapterMetadata(
|
||||
{ id: (metadataHolder as ChapterIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'category':
|
||||
return (appliedMigration) =>
|
||||
requestDeleteCategoryMetadata(
|
||||
{ id: (metadataHolder as CategoryIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'source':
|
||||
return (appliedMigration) =>
|
||||
requestDeleteSourceMetadata(
|
||||
{
|
||||
id: (metadataHolder as Pick<SourceType, 'id'>).id,
|
||||
meta: convertToGqlMeta(metadataHolder.meta),
|
||||
},
|
||||
appliedMigration,
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unexpected "type" (${type})`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevent spamming requests due to frequent metadata reads while the migration hasn't been commited to the server yet
|
||||
*/
|
||||
const commitedMigrations = new Set<string>();
|
||||
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: 'global',
|
||||
metadataHolder: MetadataHolder,
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
): METADATA => {
|
||||
useEffectFn?: typeof useEffect,
|
||||
): METADATA;
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: 'manga',
|
||||
metadataHolder: MangaIdInfo & MetadataHolder,
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): METADATA;
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: 'chapter',
|
||||
metadataHolder: ChapterIdInfo & MetadataHolder,
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): METADATA;
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: 'category',
|
||||
metadataHolder: CategoryIdInfo & MetadataHolder,
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): METADATA;
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: 'source',
|
||||
metadataHolder: Pick<SourceType, 'id'> & MetadataHolder,
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): METADATA;
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: MetadataHolderType,
|
||||
metadataHolder:
|
||||
| MetadataHolder
|
||||
| (MangaIdInfo & MetadataHolder)
|
||||
| (ChapterIdInfo & MetadataHolder)
|
||||
| (CategoryIdInfo & MetadataHolder)
|
||||
| (Pick<SourceType, 'id'> & MetadataHolder),
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
useEffectFn: typeof useEffect = (fn: () => void) => fn(),
|
||||
): METADATA {
|
||||
const wasMigrated =
|
||||
!!metadataHolder?.meta &&
|
||||
!!applyMigrations &&
|
||||
Number(getMetadataValueFrom(metadataHolder, 'migration')) !== METADATA_MIGRATIONS.length;
|
||||
const appMetadata = {} as METADATA;
|
||||
|
||||
Object.entries(metadataWithDefaultValues).forEach(([key, defaultValue]) => {
|
||||
appMetadata[key as AppMetadataKeys] = getMetadataValueFrom(
|
||||
{ meta },
|
||||
metadataHolder,
|
||||
key as AppMetadataKeys,
|
||||
defaultValue,
|
||||
applyMigrations,
|
||||
);
|
||||
});
|
||||
|
||||
useEffectFn(() => {
|
||||
const itemMigrationKey = `${type}_${type === 'global' ? '' : (metadataHolder as { id: any }).id}`;
|
||||
const commitMigration = !commitedMigrations.has(itemMigrationKey);
|
||||
if (wasMigrated && commitMigration) {
|
||||
const metadataToDelete = Object.keys(metadataHolder.meta ?? {})
|
||||
.map(extractOriginalKey)
|
||||
.filter((key) => !VALID_APP_METADATA_KEYS.includes(key)) as AppMetadataKeys[];
|
||||
|
||||
commitedMigrations.add(itemMigrationKey);
|
||||
|
||||
getMetadataDeleteFunction(
|
||||
type,
|
||||
metadataHolder,
|
||||
)(metadataToDelete)
|
||||
.then(() =>
|
||||
getMetadataUpdateFunction(type, metadataHolder)([['migration', METADATA_MIGRATIONS.length]]),
|
||||
)
|
||||
.catch((error) => {
|
||||
defaultPromiseErrorHandler(`MetadataReader#getMetadataFromServer`)(error);
|
||||
commitedMigrations.delete(itemMigrationKey);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return appMetadata;
|
||||
};
|
||||
}
|
||||
|
||||
export const getAppMetadataFrom = (meta: Metadata, appPrefix: string = APP_METADATA_KEY_PREFIX): Metadata => {
|
||||
const appMetadata: Metadata = {};
|
||||
|
||||
@@ -15,12 +15,11 @@ import {
|
||||
AllowedMetadataValueTypes,
|
||||
AppMetadataKeys,
|
||||
GqlMetaHolder,
|
||||
MetadataHolderType,
|
||||
MetadataKeyValuePair,
|
||||
} from '@/modules/metadata/Metadata.types.ts';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
|
||||
type MetadataHolderType = 'manga' | 'chapter' | 'category' | 'global' | 'source';
|
||||
|
||||
export const requestUpdateMetadataValue = async (
|
||||
metadataHolder: GqlMetaHolder,
|
||||
holderType: MetadataHolderType,
|
||||
@@ -80,3 +79,62 @@ export const requestUpdateSourceMetadata = async (
|
||||
source: Pick<SourceType, 'id'> & GqlMetaHolder,
|
||||
keysToValue: MetadataKeyValuePair[],
|
||||
): Promise<void[]> => requestUpdateMetadata(source, 'source', keysToValue);
|
||||
|
||||
export const requestDeleteMetadataValue = async (
|
||||
metadataHolder: GqlMetaHolder,
|
||||
holderType: MetadataHolderType,
|
||||
key: AppMetadataKeys,
|
||||
): Promise<void> => {
|
||||
const metadataKey = getMetadataKey(key);
|
||||
|
||||
switch (holderType) {
|
||||
case 'category':
|
||||
await requestManager.deleteCategoryMeta((metadataHolder as CategoryIdInfo).id, metadataKey).response;
|
||||
break;
|
||||
case 'chapter':
|
||||
await requestManager.deleteChapterMeta((metadataHolder as ChapterIdInfo).id, metadataKey).response;
|
||||
break;
|
||||
case 'global':
|
||||
await requestManager.deleteGlobalMeta(metadataKey).response;
|
||||
break;
|
||||
case 'manga':
|
||||
await requestManager.deleteMangaMeta((metadataHolder as MangaIdInfo).id, metadataKey).response;
|
||||
break;
|
||||
case 'source':
|
||||
await requestManager.deleteSourceMeta((metadataHolder as Pick<SourceType, 'id'>).id, metadataKey).response;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`requestDeleteMetadataValue: unknown holderType "${holderType}"`);
|
||||
}
|
||||
};
|
||||
|
||||
export async function requestDeleteMetadata(
|
||||
metadataHolder: GqlMetaHolder,
|
||||
holderType: MetadataHolderType,
|
||||
keys: AppMetadataKeys[],
|
||||
): Promise<void[]> {
|
||||
return Promise.all(keys.map((key) => requestDeleteMetadataValue(metadataHolder, holderType, key)));
|
||||
}
|
||||
|
||||
export const requestDeleteServerMetadata = async (keys: AppMetadataKeys[]): Promise<void[]> =>
|
||||
requestDeleteMetadata({}, 'global', keys);
|
||||
|
||||
export const requestDeleteMangaMetadata = async (
|
||||
manga: MangaIdInfo & GqlMetaHolder,
|
||||
keys: AppMetadataKeys[],
|
||||
): Promise<void[]> => requestDeleteMetadata(manga, 'manga', keys);
|
||||
|
||||
export const requestDeleteChapterMetadata = async (
|
||||
chapter: ChapterIdInfo & GqlMetaHolder,
|
||||
keys: AppMetadataKeys[],
|
||||
): Promise<void[]> => requestDeleteMetadata(chapter, 'chapter', keys);
|
||||
|
||||
export const requestDeleteCategoryMetadata = async (
|
||||
category: CategoryIdInfo & GqlMetaHolder,
|
||||
keys: AppMetadataKeys[],
|
||||
): Promise<void[]> => requestDeleteMetadata(category, 'category', keys);
|
||||
|
||||
export const requestDeleteSourceMetadata = async (
|
||||
source: Pick<SourceType, 'id'> & GqlMetaHolder,
|
||||
keys: AppMetadataKeys[],
|
||||
): Promise<void[]> => requestDeleteMetadata(source, 'source', keys);
|
||||
|
||||
Reference in New Issue
Block a user