UNPKG

13.2 kBPlain TextView Raw
1import * as querystring from 'querystring';
2import { IncomingMessage, IncomingHttpHeaders, Agent as HttpAgent } from 'http';
3import { Agent as HttpsAgent } from 'https';
4import { Readable } from 'stream';
5import * as httpx from 'httpx';
6import { parse } from 'url';
7
8type TeaDict = { [key: string]: string };
9type TeaObject = { [key: string]: any };
10type AgentOptions = { keepAlive: boolean };
11
12export 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
30export 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
45export 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
73function 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
90function isModelClass(t: any): boolean {
91 if (!t) {
92 return false;
93 }
94 return typeof t.types === 'function' && typeof t.names === 'function';
95}
96
97export 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 // keepAlive: default true
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
159class 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
178export function newError(data: any): ResponseError {
179 return new ResponseError(data);
180}
181
182function getValue(type: any, value: any): any {
183 if (typeof type === 'string') {
184 // basic type
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
204export 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 // 如果是另一个版本的 tea-typescript 创建的 model,instanceof 会判断不通过
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
228export 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
265export 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
359export function sleep(ms: number): Promise<void> {
360 return new Promise((resolve) => {
361 setTimeout(resolve, ms);
362 });
363}
364
365export 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
399export 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
443class UnretryableError extends Error {
444 data: any
445
446 constructor(message: string) {
447 super(message);
448 this.name = 'UnretryableError';
449 }
450}
451
452export function newUnretryableError(request: Request): Error {
453 var e = new UnretryableError('');
454 e.data = {
455 lastRequest: request
456 };
457 return e;
458}
459
460class RetryError extends Error {
461 retryable: boolean
462 data: any
463
464 constructor(message: string) {
465 super(message);
466 this.name = 'RetryError';
467 }
468}
469
470export 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
479export function isRetryable(err: Error): boolean {
480 if (typeof err === 'undefined' || err === null) {
481 return false;
482 }
483 return err.name === 'RetryError';
484}