1 | import * as querystring from 'querystring';
|
2 | import { IncomingMessage, IncomingHttpHeaders, Agent as HttpAgent } from 'http';
|
3 | import { Agent as HttpsAgent } from 'https';
|
4 | import { Readable } from 'stream';
|
5 | import * as httpx from 'httpx';
|
6 | import { parse } from 'url';
|
7 |
|
8 | type TeaDict = { [key: string]: string };
|
9 | type TeaObject = { [key: string]: any };
|
10 | type AgentOptions = { keepAlive: boolean };
|
11 |
|
12 | export class BytesReadable extends Readable {
|
13 | value: Buffer
|
14 |
|
15 | constructor(value: string | Buffer) {
|
16 | super();
|
17 | if (typeof value === 'string') {
|
18 | this.value = Buffer.from(value);
|
19 | } else if (Buffer.isBuffer(value)) {
|
20 | this.value = value;
|
21 | }
|
22 | }
|
23 |
|
24 | _read() {
|
25 | this.push(this.value);
|
26 | this.push(null);
|
27 | }
|
28 | }
|
29 |
|
30 | export class Request {
|
31 | protocol: string;
|
32 | port: number;
|
33 | method: string;
|
34 | pathname: string;
|
35 | query: TeaDict;
|
36 | headers: TeaDict;
|
37 | body: Readable;
|
38 |
|
39 | constructor() {
|
40 | this.headers = {};
|
41 | this.query = {};
|
42 | }
|
43 | }
|
44 |
|
45 | export class Response {
|
46 | statusCode: number;
|
47 | statusMessage: string;
|
48 | headers: TeaDict;
|
49 | body: IncomingMessage;
|
50 | constructor(httpResponse: IncomingMessage) {
|
51 | this.statusCode = httpResponse.statusCode;
|
52 | this.statusMessage = httpResponse.statusMessage;
|
53 | this.headers = this.convertHeaders(httpResponse.headers);
|
54 | this.body = httpResponse;
|
55 | }
|
56 |
|
57 | convertHeaders(headers: IncomingHttpHeaders): TeaDict {
|
58 | let results: TeaDict = {};
|
59 | const keys = Object.keys(headers);
|
60 | for (let index = 0; index < keys.length; index++) {
|
61 | const key = keys[index];
|
62 | results[key] = <string>headers[key];
|
63 | }
|
64 | return results;
|
65 | }
|
66 |
|
67 | async readBytes(): Promise<Buffer> {
|
68 | let buff = await httpx.read(this.body, '');
|
69 | return <Buffer>buff;
|
70 | }
|
71 | }
|
72 |
|
73 | function buildURL(request: Request) {
|
74 | let url = `${request.protocol}://${request.headers['host']}`;
|
75 | if (request.port) {
|
76 | url += `:${request.port}`;
|
77 | }
|
78 | url += `${request.pathname}`;
|
79 | const urlInfo = parse(url);
|
80 | if (request.query && Object.keys(request.query).length > 0) {
|
81 | if (urlInfo.query) {
|
82 | url += `&${querystring.stringify(request.query)}`;
|
83 | } else {
|
84 | url += `?${querystring.stringify(request.query)}`;
|
85 | }
|
86 | }
|
87 | return url;
|
88 | }
|
89 |
|
90 | function isModelClass(t: any): boolean {
|
91 | if (!t) {
|
92 | return false;
|
93 | }
|
94 | return typeof t.types === 'function' && typeof t.names === 'function';
|
95 | }
|
96 |
|
97 | export async function doAction(request: Request, runtime: TeaObject = null): Promise<Response> {
|
98 | let url = buildURL(request);
|
99 | let method = (request.method || 'GET').toUpperCase();
|
100 | let options: httpx.Options = {
|
101 | method: method,
|
102 | headers: request.headers
|
103 | };
|
104 |
|
105 | if (method !== 'GET' && method !== 'HEAD') {
|
106 | options.data = request.body;
|
107 | }
|
108 |
|
109 | if (runtime) {
|
110 | if (typeof runtime.timeout !== 'undefined') {
|
111 | options.timeout = Number(runtime.timeout);
|
112 | }
|
113 |
|
114 | if (typeof runtime.readTimeout !== 'undefined') {
|
115 | options.readTimeout = Number(runtime.readTimeout);
|
116 | }
|
117 |
|
118 | if (typeof runtime.connectTimeout !== 'undefined') {
|
119 | options.connectTimeout = Number(runtime.connectTimeout);
|
120 | }
|
121 |
|
122 | if (typeof runtime.ignoreSSL !== 'undefined') {
|
123 | options.rejectUnauthorized = !runtime.ignoreSSL;
|
124 | }
|
125 |
|
126 | if (typeof runtime.key !== 'undefined') {
|
127 | options.key = String(runtime.key);
|
128 | }
|
129 |
|
130 | if (typeof runtime.cert !== 'undefined') {
|
131 | options.cert = String(runtime.cert);
|
132 | }
|
133 |
|
134 | if (typeof runtime.ca !== 'undefined') {
|
135 | options.ca = String(runtime.ca);
|
136 | }
|
137 |
|
138 |
|
139 | let agentOptions: AgentOptions = {
|
140 | keepAlive: true,
|
141 | };
|
142 | if (typeof runtime.keepAlive !== 'undefined') {
|
143 | agentOptions.keepAlive = runtime.keepAlive;
|
144 | if (request.protocol && request.protocol.toLowerCase() === 'https') {
|
145 | options.agent = new HttpsAgent(agentOptions);
|
146 | } else {
|
147 | options.agent = new HttpAgent(agentOptions);
|
148 | }
|
149 | }
|
150 |
|
151 |
|
152 | }
|
153 |
|
154 | let response = await httpx.request(url, options);
|
155 |
|
156 | return new Response(response);
|
157 | }
|
158 |
|
159 | class ResponseError extends Error {
|
160 | code: string
|
161 | statusCode: number
|
162 | data: any
|
163 | description: string
|
164 | accessDeniedDetail: any
|
165 |
|
166 | constructor(map: any) {
|
167 | super(`${map.code}: ${map.message}`);
|
168 | this.code = map.code;
|
169 | this.data = map.data;
|
170 | this.description = map.description;
|
171 | this.accessDeniedDetail = map.accessDeniedDetail;
|
172 | if (this.data && this.data.statusCode) {
|
173 | this.statusCode = Number(this.data.statusCode);
|
174 | }
|
175 | }
|
176 | }
|
177 |
|
178 | export function newError(data: any): ResponseError {
|
179 | return new ResponseError(data);
|
180 | }
|
181 |
|
182 | function getValue(type: any, value: any): any {
|
183 | if (typeof type === 'string') {
|
184 |
|
185 | return value;
|
186 | }
|
187 | if (type.type === 'array') {
|
188 | if (!Array.isArray(value)) {
|
189 | throw new Error(`expect: array, actual: ${typeof value}`);
|
190 | }
|
191 | return value.map((item: any) => {
|
192 | return getValue(type.itemType, item);
|
193 | });
|
194 | }
|
195 | if (typeof type === 'function') {
|
196 | if (isModelClass(type)) {
|
197 | return new type(value);
|
198 | }
|
199 | return value;
|
200 | }
|
201 | return value;
|
202 | }
|
203 |
|
204 | export function toMap(value: any = undefined): any {
|
205 | if (typeof value === 'undefined' || value == null) {
|
206 | return null;
|
207 | }
|
208 |
|
209 | if (value instanceof Model) {
|
210 | return value.toMap();
|
211 | }
|
212 |
|
213 |
|
214 |
|
215 | if (typeof value.toMap === 'function') {
|
216 | return value.toMap();
|
217 | }
|
218 |
|
219 | if (Array.isArray(value)) {
|
220 | return value.map((item) => {
|
221 | return toMap(item);
|
222 | })
|
223 | }
|
224 |
|
225 | return value;
|
226 | }
|
227 |
|
228 | export class Model {
|
229 | [key: string]: any
|
230 |
|
231 | constructor(map?: TeaObject) {
|
232 | if (map == null) {
|
233 | return;
|
234 | }
|
235 |
|
236 | let clz = <any>this.constructor;
|
237 | let names = <TeaDict>clz.names();
|
238 | let types = <TeaObject>clz.types();
|
239 | Object.keys(names).forEach((name => {
|
240 | let value = map[name];
|
241 | if (value === undefined || value === null) {
|
242 | return;
|
243 | }
|
244 | let type = types[name];
|
245 | this[name] = getValue(type, value);
|
246 | }));
|
247 | }
|
248 |
|
249 | toMap(): TeaObject {
|
250 | const map: TeaObject = {};
|
251 | let clz = <any>this.constructor;
|
252 | let names = <TeaDict>clz.names();
|
253 | Object.keys(names).forEach((name => {
|
254 | const originName = names[name];
|
255 | const value = this[name];
|
256 | if (typeof value === 'undefined' || value == null) {
|
257 | return;
|
258 | }
|
259 | map[originName] = toMap(value);
|
260 | }));
|
261 | return map;
|
262 | }
|
263 | }
|
264 |
|
265 | export function cast<T>(obj: any, t: T): T {
|
266 | if (!obj) {
|
267 | throw new Error('can not cast to Map');
|
268 | }
|
269 |
|
270 | if (typeof obj !== 'object') {
|
271 | throw new Error('can not cast to Map');
|
272 | }
|
273 |
|
274 | let map = obj as TeaObject;
|
275 | let clz = t.constructor as any;
|
276 | let names: TeaDict = clz.names();
|
277 | let types: TeaObject = clz.types();
|
278 | Object.keys(names).forEach((key) => {
|
279 | let originName = names[key];
|
280 | let value = map[originName];
|
281 | let type = types[key];
|
282 | if (typeof value === 'undefined' || value == null) {
|
283 | return;
|
284 | }
|
285 | if (typeof type === 'string') {
|
286 | if (type === 'Readable' ||
|
287 | type === 'map' ||
|
288 | type === 'Buffer' ||
|
289 | type === 'any' ||
|
290 | typeof value === type) {
|
291 | (<any>t)[key] = value;
|
292 | return;
|
293 | }
|
294 | if (type === 'string' &&
|
295 | (typeof value === 'number' ||
|
296 | typeof value === 'boolean')) {
|
297 | (<any>t)[key] = value.toString();
|
298 | return;
|
299 | }
|
300 | if (type === 'boolean') {
|
301 | if (value === 1 || value === 0) {
|
302 | (<any>t)[key] = !!value;
|
303 | return;
|
304 | }
|
305 | if (value === 'true' || value === 'false') {
|
306 | (<any>t)[key] = value === 'true';
|
307 | return;
|
308 | }
|
309 | }
|
310 |
|
311 | if (type === 'number' && typeof value === 'string') {
|
312 | if (value.match(/^\d*$/)) {
|
313 | (<any>t)[key] = parseInt(value);
|
314 | return;
|
315 | }
|
316 | if (value.match(/^[\.\d]*$/)) {
|
317 | (<any>t)[key] = parseFloat(value);
|
318 | return;
|
319 | }
|
320 | }
|
321 | throw new Error(`type of ${key} is mismatch, expect ${type}, but ${typeof value}`);
|
322 | } else if (type.type === 'map') {
|
323 | if (!(value instanceof Object)) {
|
324 | throw new Error(`type of ${key} is mismatch, expect object, but ${typeof value}`);
|
325 | }
|
326 | (<any>t)[key] = value;
|
327 | } else if (type.type === 'array') {
|
328 | if (!Array.isArray(value)) {
|
329 | throw new Error(`type of ${key} is mismatch, expect array, but ${typeof value}`);
|
330 | }
|
331 | if (typeof type.itemType === 'function') {
|
332 | (<any>t)[key] = value.map((d: any) => {
|
333 | if (isModelClass(type.itemType)) {
|
334 | return cast(d, new type.itemType({}));
|
335 | }
|
336 | return d;
|
337 | });
|
338 | } else {
|
339 | (<any>t)[key] = value;
|
340 | }
|
341 |
|
342 | } else if (typeof type === 'function') {
|
343 | if (!(value instanceof Object)) {
|
344 | throw new Error(`type of ${key} is mismatch, expect object, but ${typeof value}`);
|
345 | }
|
346 | if (isModelClass(type)) {
|
347 | (<any>t)[key] = cast(value, new type({}));
|
348 | return;
|
349 | }
|
350 | (<any>t)[key] = value;
|
351 | } else {
|
352 |
|
353 | }
|
354 | });
|
355 |
|
356 | return t;
|
357 | }
|
358 |
|
359 | export function sleep(ms: number): Promise<void> {
|
360 | return new Promise((resolve) => {
|
361 | setTimeout(resolve, ms);
|
362 | });
|
363 | }
|
364 |
|
365 | export function allowRetry(retry: TeaObject, retryTimes: number, startTime: number): boolean {
|
366 |
|
367 | if (retryTimes === 0) {
|
368 | return true;
|
369 | }
|
370 |
|
371 | if (retry.retryable !== true) {
|
372 | return false;
|
373 | }
|
374 |
|
375 | if (retry.policy === 'never') {
|
376 | return false;
|
377 | }
|
378 |
|
379 | if (retry.policy === 'always') {
|
380 | return true;
|
381 | }
|
382 |
|
383 | if (retry.policy === 'simple') {
|
384 | return (retryTimes < retry['maxAttempts']);
|
385 | }
|
386 |
|
387 | if (retry.policy === 'timeout') {
|
388 | return Date.now() - startTime < retry.timeout;
|
389 | }
|
390 |
|
391 | if (retry.maxAttempts && typeof retry.maxAttempts === 'number') {
|
392 | return retry.maxAttempts >= retryTimes;
|
393 | }
|
394 |
|
395 |
|
396 | return false;
|
397 | }
|
398 |
|
399 | export function getBackoffTime(backoff: TeaObject, retryTimes: number): number {
|
400 | if (retryTimes === 0) {
|
401 |
|
402 | return 0;
|
403 | }
|
404 |
|
405 | if (backoff.policy === 'no') {
|
406 |
|
407 | return 0;
|
408 | }
|
409 |
|
410 | if (backoff.policy === 'fixed') {
|
411 |
|
412 | return backoff.period;
|
413 | }
|
414 |
|
415 | if (backoff.policy === 'random') {
|
416 |
|
417 | let min = backoff['minPeriod'];
|
418 | let max = backoff['maxPeriod'];
|
419 | return min + (max - min) * Math.random();
|
420 | }
|
421 |
|
422 | if (backoff.policy === 'exponential') {
|
423 |
|
424 | let init = backoff.initial;
|
425 | let multiplier = backoff.multiplier;
|
426 | let time = init * Math.pow(1 + multiplier, retryTimes - 1);
|
427 | let max = backoff.max;
|
428 | return Math.min(time, max);
|
429 | }
|
430 |
|
431 | if (backoff.policy === 'exponential_random') {
|
432 |
|
433 | let init = backoff.initial;
|
434 | let multiplier = backoff.multiplier;
|
435 | let time = init * Math.pow(1 + multiplier, retryTimes - 1);
|
436 | let max = backoff.max;
|
437 | return Math.min(time * (0.5 + Math.random()), max);
|
438 | }
|
439 |
|
440 | return 0;
|
441 | }
|
442 |
|
443 | class UnretryableError extends Error {
|
444 | data: any
|
445 |
|
446 | constructor(message: string) {
|
447 | super(message);
|
448 | this.name = 'UnretryableError';
|
449 | }
|
450 | }
|
451 |
|
452 | export function newUnretryableError(request: Request): Error {
|
453 | var e = new UnretryableError('');
|
454 | e.data = {
|
455 | lastRequest: request
|
456 | };
|
457 | return e;
|
458 | }
|
459 |
|
460 | class RetryError extends Error {
|
461 | retryable: boolean
|
462 | data: any
|
463 |
|
464 | constructor(message: string) {
|
465 | super(message);
|
466 | this.name = 'RetryError';
|
467 | }
|
468 | }
|
469 |
|
470 | export function retryError(request: Request, response: Response): Error {
|
471 | let e = new RetryError('');
|
472 | e.data = {
|
473 | request: request,
|
474 | response: response
|
475 | };
|
476 | return e;
|
477 | }
|
478 |
|
479 | export function isRetryable(err: Error): boolean {
|
480 | if (typeof err === 'undefined' || err === null) {
|
481 | return false;
|
482 | }
|
483 | return err.name === 'RetryError';
|
484 | }
|