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 = {
GlobalMetaType : { keyFields : [ '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 ;
}
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 = {
2023-09-30 16:55:35 +02:00
. . . existing ,
2023-11-25 15:35:26 +01:00
pageInfo : {
. . . existing . pageInfo ,
endCursor : incoming.pageInfo.endCursor ,
hasNextPage : incoming.pageInfo.hasNextPage ,
} ,
nodes : [ . . . existing . nodes , . . . incoming . nodes ] ,
2023-09-30 16:55:35 +02:00
} ;
2023-11-25 15:35:26 +01:00
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 ;
public declare client : ApolloClient < NormalizedCacheObject > ;
2023-10-04 20:50:11 +02:00
private wsClient ! : Client ;
2023-09-01 20:21:59 +02:00
public override getBaseUrl ( ) : string {
return ` ${ super . getBaseUrl ( ) } /api/graphql ` ;
}
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 ,
2024-01-26 20:55:14 +01:00
} ) ;
2024-01-21 22:31:20 +01: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 ( ) ;
2023-10-04 20:50:11 +02:00
} ) ;
2024-01-26 20:55:14 +01:00
const checkHeartbeatInterval = heartbeatInterval + 1000 * 10 ;
// for some reason "this.wsClient" is undefined in the "setInterval" callback
const { wsClient } = this ;
setInterval ( ( ) = > {
const isHeartbeatMissing = Date . now ( ) - lastHeartbeat > checkHeartbeatInterval * 1.1 ;
if ( isHeartbeatMissing ) {
// force a reconnect
wsClient . terminate ( ) ;
}
} , 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() { }
}