1
+ import PQueue from "p-queue" ;
2
+ import rateLimiter from "rolling-rate-limiter" ;
3
+ import { v4 as uuid } from "uuid" ;
4
+
1
5
import * as Rest from "./rest" ;
6
+ import { IPC_MAIN_API } from "src/shared/api/main" ;
2
7
import { Unpacked } from "src/shared/types" ;
3
8
import { WEBVIEW_LOGGERS } from "src/electron-preload/webview/constants" ;
4
- import { curryFunctionMembers } from "src/shared/util" ;
9
+ import { asyncDelay , curryFunctionMembers } from "src/shared/util" ;
5
10
6
11
// TODO consider executing direct $http calls
7
12
// in order to not depend on Protonmail WebClient's AngularJS factories/services
@@ -10,6 +15,9 @@ export interface Api {
10
15
url : {
11
16
build : ( module : string ) => ( ) => string ;
12
17
} ;
18
+ lazyLoader : {
19
+ app : ( ) => Promise < void > ;
20
+ } ;
13
21
messageModel : ( message : Rest . Model . Message ) => {
14
22
clearTextBody : ( ) => Promise < string > ;
15
23
} ;
@@ -43,13 +51,37 @@ export interface Api {
43
51
}
44
52
45
53
const logger = curryFunctionMembers ( WEBVIEW_LOGGERS . protonmail , "[lib/api]" ) ;
54
+ const resolveServiceLogger = curryFunctionMembers ( logger , "resolveService()" ) ;
55
+ const rateLimitedApiCallingQueue : PQueue < PQueue . DefaultAddOptions > = new PQueue ( { concurrency : 1 } ) ;
56
+ const ipcMainApiClient = IPC_MAIN_API . buildClient ( ) ;
46
57
const state : { api ?: Promise < Api > } = { } ;
47
58
59
+ let rateLimitedMethodsCallCount = 0 ;
60
+
48
61
export async function resolveApi ( ) : Promise < Api > {
62
+ logger . info ( `resolveApi()` ) ;
49
63
if ( state . api ) {
50
64
return state . api ;
51
65
}
52
66
67
+ const rateLimiting = {
68
+ rateLimiterTick : await ( async ( ) => {
69
+ const { fetchingRateLimiting : config } = await ipcMainApiClient ( "readConfig" ) ( ) . toPromise ( ) ;
70
+ logger . debug ( JSON . stringify ( {
71
+ fetchingRateLimiter_111 : {
72
+ interval : config . intervalMs ,
73
+ maxInInterval : config . maxInInterval ,
74
+ } ,
75
+ } ) ) ;
76
+ const limiter = rateLimiter ( {
77
+ interval : config . intervalMs ,
78
+ maxInInterval : config . maxInInterval ,
79
+ } ) ;
80
+ const key = `webview:protonmail-api:${ uuid ( ) } ` ;
81
+ return ( ) => limiter ( key ) ;
82
+ } ) ( ) ,
83
+ } ;
84
+
53
85
return state . api = ( async ( ) => {
54
86
const angular : angular . IAngularStatic | undefined = ( window as any ) . angular ;
55
87
const injector = angular && angular . element ( document . body ) . injector ( ) ;
@@ -58,34 +90,99 @@ export async function resolveApi(): Promise<Api> {
58
90
throw new Error ( `Failed to resolve "injector" variable` ) ;
59
91
}
60
92
61
- await injectorGet < { app : ( ) => Promise < void > } > ( injector , "lazyLoader" ) . app ( ) ;
93
+ const lazyLoader = resolveService < Api [ "lazyLoader" ] > ( injector , "lazyLoader" ) ;
94
+
95
+ await lazyLoader . app ( ) ;
62
96
63
97
// TODO validate types of all the described constants/functions in a declarative way
64
98
// so app gets protonmail breaking changes noticed on early stage
65
99
66
100
return {
67
- $http : injectorGet < Api [ "$http" ] > ( injector , "$http" ) ,
68
- url : injectorGet < Api [ "url" ] > ( injector , "url" ) ,
69
- messageModel : injectorGet < Api [ "messageModel" ] > ( injector , "messageModel" ) ,
70
- conversation : injectorGet < Api [ "conversation" ] > ( injector , "conversationApi" ) ,
71
- message : injectorGet < Api [ "message" ] > ( injector , "messageApi" ) ,
72
- contact : injectorGet < Api [ "contact" ] > ( injector , "Contact" ) ,
73
- label : injectorGet < Api [ "label" ] > ( injector , "Label" ) ,
74
- events : injectorGet < Api [ "events" ] > ( injector , "Events" ) ,
75
- vcard : injectorGet < Api [ "vcard" ] > ( injector , "vcard" ) ,
101
+ lazyLoader,
102
+ $http : resolveService < Api [ "$http" ] > ( injector , "$http" ) ,
103
+ url : resolveService < Api [ "url" ] > ( injector , "url" ) ,
104
+ messageModel : resolveService < Api [ "messageModel" ] > ( injector , "messageModel" ) ,
105
+ conversation : resolveService < Api [ "conversation" ] > ( injector , "conversationApi" , { ...rateLimiting , methods : [ "get" , "query" ] } ) ,
106
+ message : resolveService < Api [ "message" ] > ( injector , "messageApi" , { ...rateLimiting , methods : [ "get" , "query" ] } ) ,
107
+ contact : resolveService < Api [ "contact" ] > ( injector , "Contact" , { ...rateLimiting , methods : [ "get" , "all" ] } ) ,
108
+ label : resolveService < Api [ "label" ] > ( injector , "Label" , { ...rateLimiting , methods : [ "query" ] } ) ,
109
+ events : resolveService < Api [ "events" ] > ( injector , "Events" , { ...rateLimiting , methods : [ "get" , "getLatestID" ] } ) ,
110
+ vcard : resolveService < Api [ "vcard" ] > ( injector , "vcard" ) ,
76
111
} ;
77
112
} ) ( ) ;
78
113
}
79
114
80
- function injectorGet < T > ( injector : ng . auto . IInjectorService , name : string ) : T {
81
- logger . info ( `injectorGet()` ) ;
82
- const result = injector . get < T | undefined > ( name ) ;
115
+ type KeepAsyncFunctionsProps < T > = {
116
+ [ K in keyof T ] : T [ K ] extends ( args : any ) => Promise < infer U > ? T [ K ] : never
117
+ } ;
118
+
119
+ function resolveService < T extends Api [ keyof Api ] > (
120
+ injector : ng . auto . IInjectorService ,
121
+ serviceName : string ,
122
+ rateLimiting ?: {
123
+ rateLimiterTick : ( ) => number ;
124
+ methods : Array < keyof KeepAsyncFunctionsProps < T > > ,
125
+ } ,
126
+ ) : T {
127
+ resolveServiceLogger . info ( ) ;
128
+ const service = injector . get < T | undefined > ( serviceName ) ;
129
+
130
+ if ( ! service ) {
131
+ throw new Error ( `Failed to resolve "${ serviceName } " service` ) ;
132
+ }
133
+
134
+ resolveServiceLogger . verbose ( `"${ serviceName } " keys` , JSON . stringify ( Object . keys ( service ) ) ) ;
83
135
84
- if ( ! result ) {
85
- throw new Error ( `Failed to resolve " ${ name } " service` ) ;
136
+ if ( ! rateLimiting ) {
137
+ return service ;
86
138
}
87
139
88
- logger . verbose ( `injectorGet()` , `"${ name } " keys` , JSON . stringify ( Object . keys ( result ) ) ) ;
140
+ const clonedService = { ...service } as T ;
141
+
142
+ for ( const method of rateLimiting . methods ) {
143
+ const originalMethod = clonedService [ method ] ;
144
+ const _fullMethodName = `${ serviceName } .${ method } ` ;
145
+
146
+ if ( typeof originalMethod !== "function" ) {
147
+ throw new Error ( `Not a function: "${ _fullMethodName } "` ) ;
148
+ }
149
+
150
+ clonedService [ method ] = async function ( this : typeof service ) {
151
+ const originalMethodArgs = arguments ;
152
+ const timeLeftUntilAllowed = rateLimiting . rateLimiterTick ( ) ;
153
+ const limitExceeded = timeLeftUntilAllowed > 0 ;
154
+
155
+ if ( limitExceeded ) {
156
+ resolveServiceLogger . info ( [
157
+ `delaying rate limited method calling: ` ,
158
+ JSON . stringify ( {
159
+ method : _fullMethodName ,
160
+ timeLeftUntilAllowed,
161
+ rateLimitedMethodsCallCount,
162
+ } ) ,
163
+ ] . join ( "" ) ) ;
164
+
165
+ await asyncDelay ( timeLeftUntilAllowed ) ;
166
+ }
167
+
168
+ resolveServiceLogger . debug ( `queueing rate limited method: "${ _fullMethodName } "` ) ;
169
+
170
+ return rateLimitedApiCallingQueue . add ( ( ) => {
171
+ resolveServiceLogger . verbose ( [
172
+ `calling rate limited method: ` ,
173
+ JSON . stringify ( {
174
+ method : _fullMethodName ,
175
+ timeLeftUntilAllowed,
176
+ rateLimitedMethodsCallCount,
177
+ } ) ,
178
+ ] . join ( "" ) ) ;
179
+
180
+ const result = originalMethod . apply ( service , originalMethodArgs ) ;
181
+ rateLimitedMethodsCallCount ++ ;
182
+ return result ;
183
+ } ) ;
184
+ } as any ;
185
+ }
89
186
90
- return result ;
187
+ return clonedService ;
91
188
}
0 commit comments