2023-09-01 20:21:59 +02:00
/*
* 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/.
*/
2024-04-14 15:10:34 +02:00
import {
ApolloClient ,
ApolloClientOptions ,
ApolloLink ,
InMemoryCache ,
NormalizedCacheObject ,
split ,
} from '@apollo/client' ;
2023-09-01 20:21:59 +02:00
import { createUploadLink } from 'apollo-upload-client' ;
2023-10-04 20:50:11 +02:00
import { GraphQLWsLink } from '@apollo/client/link/subscriptions' ;
import { Client , createClient } from 'graphql-ws' ;
import { getMainDefinition } from '@apollo/client/utilities' ;
2023-12-25 21:20:07 +01:00
import { TypePolicies } from '@apollo/client/cache' ;
2023-09-01 20:21:59 +02:00
import { BaseClient } from '@/lib/requests/client/BaseClient.ts' ;
import { StrictTypedTypePolicies } from '@/lib/graphql/generated/apollo-helpers.ts' ;
2023-09-30 16:55:35 +02:00
/* eslint-disable no-underscore-dangle */
2023-09-01 20:21:59 +02:00
const typePolicies : StrictTypedTypePolicies = {
2024-09-08 01:52:36 +02:00
MangaType : {
fields : {
trackRecords : {
merge ( existing , incoming ) {
const nodes = incoming . nodes ?? existing ? . nodes ;
return {
... existing ,
... incoming ,
totalCount : nodes?.length ?? existing ? . totalCount ?? incoming . totalCount ,
nodes ,
};
},
},
},
},
2023-09-01 20:21:59 +02:00
GlobalMetaType : { keyFields : [ 'key' ] },
2024-09-21 20:01:06 +02:00
MangaMetaType : { keyFields : [ 'mangaId' , 'key' ] },
ChapterMetaType : { keyFields : [ 'chapterId' , 'key' ] },
CategoryMetaType : { keyFields : [ 'categoryId' , 'key' ] },
SourceMetaType : { keyFields : [ 'sourceId' , 'key' ] },
2023-12-25 21:16:04 +01:00
ExtensionType : { keyFields : [ 'pkgName' ] },
2023-11-24 01:33:56 +01:00
AboutServerPayload : { keyFields : [] },
2023-12-09 23:08:27 +01:00
AboutWebUI : { keyFields : [] },
2023-11-24 01:33:56 +01:00
WebUIUpdateInfo : { keyFields : [] },
2023-12-09 23:08:27 +01:00
WebUIUpdateCheck : { keyFields : [] },
2023-10-27 23:39:05 +02:00
SettingsType : { keyFields : [] },
2023-12-13 02:05:03 +01:00
DownloadStatus : { keyFields : [] },
DownloadType : { keyFields : [ 'chapter' ] },
2024-01-01 22:40:39 +01:00
WebUIUpdateStatus : { keyFields : [] },
UpdateStatus : { keyFields : [] },
2023-09-30 16:55:35 +02:00
Query : {
fields : {
2023-12-25 21:20:07 +01:00
manga ( _ , { args , toReference }) {
return toReference ({
__typename : 'MangaType' ,
id : args?.id ,
});
},
category ( _ , { args , toReference }) {
return toReference ({
__typename : 'CategoryType' ,
id : args?.id ,
});
},
source ( _ , { args , toReference }) {
return toReference ({
__typename : 'SourceType' ,
id : args?.id ,
});
},
extension ( _ , { args , toReference }) {
return toReference ({
__typename : 'ExtensionType' ,
2024-07-01 20:18:48 +02:00
pkgName : args?.pkgName ,
2023-12-25 21:20:07 +01:00
});
},
meta ( _ , { args , toReference }) {
return toReference ({
__typename : 'GlobalMetaType' ,
key : args?.key ,
});
},
2024-01-01 22:40:39 +01:00
downloadStatus ( _ , { toReference }) {
return toReference ({
__typename : 'DownloadStatus' ,
key : {},
});
},
getWebUIUpdateStatus ( _ , { toReference }) {
return toReference ({
__typename : 'WebUIUpdateStatus' ,
key : {},
});
},
updateStatus ( _ , { toReference }) {
return toReference ({ __typename : 'UpdateStatus' , key : {} });
},
2023-09-30 16:55:35 +02:00
chapters : {
keyArgs : [ 'condition' , 'filter' , 'orderBy' , 'orderByType' ],
merge ( existing , incoming ) {
2023-11-25 15:35:26 +01:00
if ( existing == null ) {
return incoming ;
2023-09-30 16:55:35 +02:00
}
2023-11-25 15:35:26 +01:00
const isReFetch = ! incoming . pageInfo . hasPreviousPage ;
const hasLessItems = existing . nodes . length > incoming . nodes . length ;
const useIncomingResponse = isReFetch && ! hasLessItems ;
if ( useIncomingResponse ) {
return incoming ;
}
const replaceExistingItems = isReFetch && hasLessItems ;
if ( replaceExistingItems ) {
const existingWithReplacedIncoming : typeof incoming = {
... existing ,
pageInfo : {
... existing . pageInfo ,
startCursor : incoming.pageInfo.startCursor ,
},
nodes : [... incoming . nodes , ... existing . nodes . slice ( incoming . nodes . length )],
};
return existingWithReplacedIncoming ;
}
const existingWithAppendedIncoming : typeof incoming = {
... existing ,
pageInfo : {
... existing . pageInfo ,
endCursor : incoming.pageInfo.endCursor ,
hasNextPage : incoming.pageInfo.hasNextPage ,
},
nodes : [... existing . nodes , ... incoming . nodes ],
};
return existingWithAppendedIncoming ;
2023-09-30 16:55:35 +02:00
},
},
},
},
2023-09-01 20:21:59 +02:00
};
2023-09-30 16:55:35 +02:00
/* eslint-enable no-underscore-dangle */
2023-09-01 20:21:59 +02:00
export class GraphQLClient extends BaseClient <
ApolloClient < NormalizedCacheObject >,
ApolloClientOptions < NormalizedCacheObject >,
null
> {
readonly fetcher = null ;
2024-08-25 22:34:01 +02:00
public client !: ApolloClient < NormalizedCacheObject >;
2023-09-01 20:21:59 +02:00
2023-10-04 20:50:11 +02:00
private wsClient !: Client ;
2024-08-25 22:34:01 +02:00
constructor () {
super ();
this . createClient ();
}
2023-09-01 20:21:59 +02:00
public override getBaseUrl () : string {
return ` ${ super . getBaseUrl () } /api/graphql` ;
}
2024-08-25 22:34:01 +02:00
public terminateSubscriptions () : void {
this . wsClient . terminate ();
}
2023-09-01 20:21:59 +02:00
private createUploadLink() {
2024-04-09 18:04:30 -04:00
return createUploadLink ({ uri : () => this . getBaseUrl (), credentials : 'include' });
2023-09-01 20:21:59 +02:00
}
2023-10-04 20:50:11 +02:00
private createWSLink() {
return new GraphQLWsLink ( this . wsClient );
}
2023-09-01 20:21:59 +02:00
private createLink() {
2023-10-04 20:50:11 +02:00
return split (
({ query }) => {
const definition = getMainDefinition ( query );
return definition . kind === 'OperationDefinition' && definition . operation === 'subscription' ;
},
this . createWSLink (),
2024-04-14 15:10:34 +02:00
// apollo-upload-client dependency is outdated (see 134e47763faae9e62db4d4e3a8387a74e32e5568) and thus types are not matching, but they are still correct
this . createUploadLink () as unknown as ApolloLink ,
2023-10-04 20:50:11 +02:00
);
2023-09-01 20:21:59 +02:00
}
protected createClient() {
2024-01-26 20:55:14 +01:00
const heartbeatInterval = 1000 * 20 ;
2023-10-04 20:50:11 +02:00
this . wsClient = createClient ({
url : () => this . getBaseUrl (). replace ( /http(|s)/g , 'ws' ),
2024-01-26 20:55:14 +01:00
keepAlive : heartbeatInterval ,
2024-01-21 22:31:20 +01:00
retryAttempts : Infinity ,
2023-10-04 20:50:11 +02:00
});
2024-01-26 20:55:14 +01:00
let lastHeartbeat : number = 0 ;
this . wsClient . on ( 'connected' , () => {
lastHeartbeat = Date . now ();
});
this . wsClient . on ( 'pong' , () => {
lastHeartbeat = Date . now ();
});
const checkHeartbeatInterval = heartbeatInterval + 1000 * 10 ;
setInterval (() => {
const isHeartbeatMissing = Date . now () - lastHeartbeat > checkHeartbeatInterval * 1.1 ;
if ( isHeartbeatMissing ) {
// force a reconnect
2024-08-25 22:34:01 +02:00
this . wsClient . terminate ();
2024-01-26 20:55:14 +01:00
}
}, checkHeartbeatInterval );
2023-09-01 20:21:59 +02:00
this . client = new ApolloClient ({
cache : new InMemoryCache ({
2023-12-25 21:20:07 +01:00
// for whatever reason there is some weird TypeError complaining that
// "FieldReadFunction<Reference, Reference, FieldFunctionOptions<SomeObject, Record<string, any>>"
// is not compatible with "FieldReadFunction<any, any, FieldFunctionOptions<Record<string, any, Record<string, any>>"
// Since "typePolicies" is correctly typed as StrictTypedTypePolicies, and it is working as expected,
// the TypeError can just be ignored
typePolicies : typePolicies as TypePolicies ,
2023-09-01 20:21:59 +02:00
}),
connectToDevTools : true ,
link : this.createLink (),
});
}
public override updateConfig() { }
}