├── test ├── fixtures │ ├── empty.txt │ ├── test.txt │ └── test.json ├── func.spec.ts ├── url.spec.ts ├── date.spec.ts ├── form.spec.ts ├── error.spec.ts ├── file.spec.ts ├── stream.spec.ts ├── xml.spec.ts ├── retry.spec.ts └── core.spec.ts ├── .gitignore ├── .eslintrc ├── src ├── index.ts ├── func.ts ├── url.ts ├── error.ts ├── form.ts ├── file.ts ├── date.ts ├── xml.ts ├── stream.ts ├── retry.ts └── core.ts ├── tsconfig.json ├── .github └── workflows │ ├── ci.yml │ └── node.js.yml ├── README.md └── package.json /test/fixtures/empty.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/test.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/test.json: -------------------------------------------------------------------------------- 1 | {"key":"value"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | dist/ 5 | .DS_Store 6 | test/fixtures/newfile.txt -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 2020, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [2, 2], 13 | "no-console": 0 14 | } 15 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export { default as Date } from './date'; 3 | export * from './error'; 4 | export { default as File } from './file'; 5 | export { default as Form, FileFormStream } from './form'; 6 | export * from './func'; 7 | export * from './retry'; 8 | export { default as Stream, SSEEvent } from './stream'; 9 | export { default as URL } from './url'; 10 | export { default as XML } from './xml'; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "target": "es5", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "*": [ 14 | "node_modules/*", 15 | "src/types/*" 16 | ] 17 | } 18 | }, 19 | "include": [ 20 | "src/**/*" 21 | ] 22 | } -------------------------------------------------------------------------------- /src/func.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | export function isNull(data: any): boolean{ 5 | if (typeof data === 'undefined') { 6 | return true; 7 | } 8 | 9 | if (data === null) { 10 | return true; 11 | } 12 | 13 | return false; 14 | } 15 | 16 | export function merge(source: {[key: string]: any}, data: {[key: string]: any}): {[key: string]: any}{ 17 | if(!source && !data) { 18 | return null; 19 | } 20 | return _.merge({}, source, data); 21 | } 22 | 23 | export function sleep(ms: number): Promise { 24 | return new Promise((resolve) => { 25 | setTimeout(resolve, ms); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ 1.x ] 6 | pull_request: 7 | branches: [ 1.x ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ${{ matrix.operating-system }} 13 | strategy: 14 | matrix: 15 | operating-system: [ubuntu-latest, macos-latest] 16 | node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x] 17 | name: Node.js ${{ matrix.node-version }} Test on ${{ matrix.operating-system }} 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@v2 28 | 29 | - run: npm install 30 | - run: npm run ci 31 | - name: CodeCov 32 | run: bash <(curl -s https://codecov.io/bash) -cF tea-typescript -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Darabonba Support for TypeScript/Node.js 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Node.js CI](https://github.com/aliyun/tea-typescript/actions/workflows/node.js.yml/badge.svg)](https://github.com/aliyun/tea-typescript/actions/workflows/node.js.yml) 5 | [![codecov][cov-image]][cov-url] 6 | [![npm download][download-image]][download-url] 7 | 8 | [npm-image]: https://img.shields.io/npm/v/@darabonba/typescript.svg?style=flat-square 9 | [npm-url]: https://npmjs.org/package/@darabonba/typescript 10 | [cov-image]: https://codecov.io/gh/aliyun/tea-typescript/branch/master/graph/badge.svg 11 | [cov-url]: https://codecov.io/gh/aliyun/tea-typescript 12 | [download-image]: https://img.shields.io/npm/dm/@darabonba/typescript.svg?style=flat-square 13 | [download-url]: https://npmjs.org/package/@darabonba/typescript 14 | 15 | Darabonba support for TypeScript/Node.js. 16 | 17 | ## Installation 18 | 19 | ```bash 20 | $ npm i @darabonba/typescript -S 21 | ``` 22 | 23 | ## License 24 | The Apache License 2.0. 25 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x, 18.x, 20.x, 22.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | fail-fast: false 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: npm install 30 | - run: npm run test-cov 31 | - uses: codecov/codecov-action@v4 32 | with: 33 | token: ${{ secrets.CODECOV_TOKEN }} # required 34 | -------------------------------------------------------------------------------- /test/func.spec.ts: -------------------------------------------------------------------------------- 1 | import * as $dara from '../src/index'; 2 | import 'mocha'; 3 | import assert from 'assert'; 4 | import moment from 'moment'; 5 | 6 | describe('$dara func', function () { 7 | 8 | it('isNull should ok', function () { 9 | assert.deepStrictEqual($dara.isNull(null), true); 10 | assert.deepStrictEqual($dara.isNull(undefined), true); 11 | assert.deepStrictEqual($dara.isNull(false), false); 12 | assert.deepStrictEqual($dara.isNull({}), false); 13 | }); 14 | 15 | it('merge should ok', async function () { 16 | const obj1 = { 17 | a: 1, 18 | b: { c: 2, d: { e: 3 } }, 19 | c: 4, 20 | }; 21 | const obj2 = { 22 | b: { d: { f: 4 }, g: 5 }, 23 | h: 6, 24 | }; 25 | const expectedResult = { 26 | a: 1, 27 | b: { c: 2, d: { e: 3, f: 4 }, g: 5 }, 28 | c: 4, 29 | h: 6 30 | }; 31 | assert.deepStrictEqual($dara.merge(obj1, obj2), expectedResult); 32 | assert.deepStrictEqual($dara.merge(obj1, null), obj1); 33 | assert.deepStrictEqual($dara.merge(obj1, undefined), obj1); 34 | assert.deepStrictEqual($dara.merge(null, undefined), null); 35 | }); 36 | 37 | it('sleep should ok', async function () { 38 | const begin = moment().unix(); 39 | await $dara.sleep(1000); 40 | const end = moment().unix(); 41 | assert.deepStrictEqual(end - begin, 1); 42 | }); 43 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@darabonba/typescript", 3 | "version": "1.0.3", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:aliyun/typescript.git" 10 | }, 11 | "license": "Apache License 2.0", 12 | "scripts": { 13 | "test": "mocha -r ts-node/register -r source-map-support/register test/**/*.spec.ts --timeout=10000", 14 | "test-cov": "nyc -e .ts -r=html -r=text -r=lcov npm run test", 15 | "build": "tsc", 16 | "prepublishOnly": "tsc", 17 | "lint": "eslint . --ext .ts", 18 | "lint:fix": "eslint . --ext .ts --fix" 19 | }, 20 | "author": "Jackson Tian", 21 | "devDependencies": { 22 | "@types/lodash": "^4.14.202", 23 | "@types/mocha": "^5.2.7", 24 | "@types/node": "^20.11.10", 25 | "@types/xml2js": "^0.4.14", 26 | "@typescript-eslint/eslint-plugin": "^7.0.2", 27 | "@typescript-eslint/parser": "^7.0.2", 28 | "eslint": "^8.57.0", 29 | "mocha": "^6.2.0", 30 | "nyc": "^14.1.1", 31 | "source-map-support": "^0.5.13", 32 | "ts-node": "^8.4.1", 33 | "typescript": "^4.7.4" 34 | }, 35 | "dependencies": { 36 | "httpx": "^2.3.2", 37 | "lodash": "^4.17.21", 38 | "moment": "^2.30.1", 39 | "moment-timezone": "^0.5.45", 40 | "@alicloud/tea-typescript": "^1.5.1", 41 | "xml2js": "^0.6.2" 42 | }, 43 | "files": [ 44 | "dist", 45 | "src" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /test/url.spec.ts: -------------------------------------------------------------------------------- 1 | import * as $dara from '../src/index'; 2 | import 'mocha'; 3 | import assert from 'assert'; 4 | import moment from 'moment'; 5 | 6 | describe('$dara url', function () { 7 | 8 | it('parse should ok', function () { 9 | const url = 'https://sdk:test@ecs.aliyuncs.com:443/sdk/?api&ok=test#sddd'; 10 | const ret = $dara.URL.parse(url); 11 | assert.deepStrictEqual(ret.path(), '/sdk/?api&ok=test'); 12 | assert.deepStrictEqual(ret.pathname(), '/sdk/'); 13 | assert.deepStrictEqual(ret.protocol(), 'https'); 14 | assert.deepStrictEqual(ret.hostname(), 'ecs.aliyuncs.com'); 15 | assert.deepStrictEqual(ret.host(), 'ecs.aliyuncs.com'); 16 | assert.deepStrictEqual(ret.port(), '443'); 17 | assert.deepStrictEqual(ret.hash(), 'sddd'); 18 | assert.deepStrictEqual(ret.search(), 'api&ok=test'); 19 | assert.deepStrictEqual(ret.href(), 'https://sdk:test@ecs.aliyuncs.com/sdk/?api&ok=test#sddd'); 20 | assert.deepStrictEqual(ret.auth(), 'sdk:test'); 21 | }); 22 | 23 | it('urlEncode should ok', async function () { 24 | const result = $dara.URL.urlEncode('https://www.baidu.com/') 25 | assert.strictEqual("https%3A%2F%2Fwww.baidu.com%2F", result); 26 | }); 27 | 28 | it('percentEncode should ok', async function () { 29 | const result = $dara.URL.percentEncode('https://www.bai+*~du.com/') 30 | assert.strictEqual("https%3A%2F%2Fwww.bai%2B%2A~du.com%2F", result); 31 | }); 32 | 33 | it('pathEncode should ok', async function () { 34 | const result = $dara.URL.pathEncode("/work_space/DARABONBA/GIT/darabonba-util/ts") 35 | assert.strictEqual("/work_space/DARABONBA/GIT/darabonba-util/ts", result); 36 | }); 37 | }); -------------------------------------------------------------------------------- /test/date.spec.ts: -------------------------------------------------------------------------------- 1 | import * as $dara from '../src/index'; 2 | import moment from 'moment-timezone'; 3 | import 'mocha'; 4 | import assert from 'assert'; 5 | 6 | describe('$dara date', function () { 7 | moment.tz.setDefault('Asia/Shanghai'); 8 | 9 | it('init should be okay', () => { 10 | const date = new $dara.Date('2023-12-31 00:00:00'); 11 | const expectDate = moment('2023-12-31 00:00:00'); 12 | assert.strictEqual(date.unix(), expectDate.unix()); 13 | }); 14 | 15 | it('method should be okay', () => { 16 | const date = new $dara.Date('2023-12-31 00:00:00.916000 +0800 UTC'); 17 | assert.strictEqual(date.format("YYYY-MM-DD HH:mm:ss"), '2023-12-31 08:00:00'); 18 | assert.strictEqual(date.unix(), 1703980800); 19 | const yesterday = date.sub("day", 1); 20 | assert.strictEqual(yesterday.format("YYYY-MM-DD HH:mm:ss"), '2023-12-30 08:00:00'); 21 | assert.strictEqual(date.diff("day", yesterday), 1); 22 | const tomorrow = date.add("day", 1); 23 | assert.strictEqual(date.diff("day", tomorrow), -1); 24 | assert.strictEqual(date.hour(), 8); 25 | assert.strictEqual(date.minute(), 0); 26 | assert.strictEqual(date.second(), 0); 27 | assert.strictEqual(date.dayOfMonth(), 31); 28 | assert.strictEqual(date.dayOfWeek(), 7); 29 | assert.strictEqual(date.weekOfMonth(), 5); 30 | assert.strictEqual(tomorrow.weekOfMonth(), 1); 31 | assert.strictEqual(yesterday.weekOfMonth(), 5); 32 | assert.strictEqual(date.weekOfYear(), 52); 33 | assert.strictEqual(tomorrow.weekOfYear(), 1); 34 | assert.strictEqual(yesterday.weekOfYear(), 52); 35 | assert.strictEqual(date.month(), 12); 36 | assert.strictEqual(date.year(), 2023); 37 | assert.strictEqual(yesterday.month(), 12); 38 | assert.strictEqual(yesterday.year(), 2023); 39 | assert.strictEqual(tomorrow.month(), 1); 40 | assert.strictEqual(tomorrow.year(), 2024); 41 | }); 42 | }); -------------------------------------------------------------------------------- /src/url.ts: -------------------------------------------------------------------------------- 1 | import * as url from 'url'; 2 | 3 | type DATE_TYPE = string | Date | moment.Moment 4 | 5 | const portMap: { [key: string]: string } = { 6 | ftp: '21', 7 | gopher: '70', 8 | http: '80', 9 | https: '443', 10 | ws: '80', 11 | wss: '443', 12 | }; 13 | 14 | export default class TeaURL { 15 | _url: url.URL 16 | 17 | constructor(str: string) { 18 | this._url = new url.URL(str); 19 | } 20 | 21 | path(): string { 22 | return this._url.pathname + this._url.search; 23 | } 24 | 25 | pathname(): string { 26 | return this._url.pathname; 27 | } 28 | 29 | protocol(): string { 30 | return this._url.protocol ? this._url.protocol.replace(':', '') : ''; 31 | } 32 | 33 | hostname(): string { 34 | return this._url.hostname; 35 | } 36 | 37 | host(): string { 38 | return this._url.host; 39 | } 40 | 41 | port(): string { 42 | return this._url.port || portMap[this.protocol()]; 43 | } 44 | 45 | hash(): string { 46 | return this._url.hash ? this._url.hash.replace('#', '') : ''; 47 | } 48 | 49 | search(): string { 50 | return this._url.search ? this._url.search.replace('?', '') : ''; 51 | } 52 | 53 | href(): string { 54 | return this._url.href; 55 | } 56 | 57 | auth(): string { 58 | return `${this._url.username}:${this._url.password}`; 59 | } 60 | 61 | static parse(url: string): TeaURL { 62 | return new TeaURL(url); 63 | } 64 | 65 | static urlEncode(url: string): string { 66 | return url != null ? encodeURIComponent(url) : ''; 67 | } 68 | 69 | static percentEncode(raw: string): string { 70 | return raw != null ? encodeURIComponent(raw).replace('+', '%20') 71 | .replace('*', '%2A').replace('%7E', '~') : null; 72 | } 73 | 74 | static pathEncode(path: string): string { 75 | if (!path || path === '/') { 76 | return path; 77 | } 78 | const paths = path.split('/'); 79 | const sb = []; 80 | for (const s of paths) { 81 | sb.push(TeaURL.percentEncode(s)); 82 | } 83 | return sb.join('/'); 84 | } 85 | } -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { Request, Response } from './core'; 4 | import { RetryPolicyContext } from './retry'; 5 | 6 | export class BaseError extends Error { 7 | name: string; 8 | code: string; 9 | 10 | constructor(map: { [key: string]: any }) { 11 | super(`${map.code}: ${map.message}`); 12 | this.name = 'BaseError'; 13 | this.code = map.code; 14 | } 15 | } 16 | 17 | export class ResponseError extends BaseError { 18 | code: string 19 | statusCode?: number 20 | retryAfter?: number 21 | data?: any 22 | description?: string 23 | accessDeniedDetail?: any 24 | 25 | constructor(map: any) { 26 | super(map); 27 | this.name = 'ResponseError'; 28 | this.data = map.data; 29 | this.description = map.description; 30 | this.retryAfter = map.retryAfter; 31 | this.accessDeniedDetail = map.accessDeniedDetail; 32 | if (this.data && this.data.statusCode) { 33 | this.statusCode = Number(this.data.statusCode); 34 | } 35 | } 36 | } 37 | 38 | 39 | 40 | class UnretryableError extends Error { 41 | data: any 42 | 43 | constructor(message: string) { 44 | super(message); 45 | this.name = 'UnretryableError'; 46 | } 47 | } 48 | 49 | class RetryError extends Error { 50 | retryable: boolean 51 | data: any 52 | 53 | constructor(message: string) { 54 | super(message); 55 | this.name = 'RetryError'; 56 | } 57 | } 58 | 59 | export function retryError(request: Request, response: Response): Error { 60 | const e = new RetryError(''); 61 | e.data = { 62 | request: request, 63 | response: response 64 | }; 65 | return e; 66 | } 67 | 68 | 69 | export function newError(data: any): ResponseError { 70 | return new ResponseError(data); 71 | } 72 | 73 | export function newUnretryableError(ctx: RetryPolicyContext | Request): Error { 74 | if(ctx instanceof RetryPolicyContext && ctx.exception) { 75 | return ctx.exception; 76 | } else { 77 | const e = new UnretryableError(''); 78 | e.data = { 79 | lastRequest: ctx 80 | }; 81 | return e; 82 | } 83 | } -------------------------------------------------------------------------------- /test/form.spec.ts: -------------------------------------------------------------------------------- 1 | import * as $dara from '../src/index'; 2 | import 'mocha'; 3 | import assert from 'assert'; 4 | import { createReadStream } from 'fs'; 5 | import { join } from 'path'; 6 | import { Readable } from 'stream'; 7 | 8 | async function read(readable: Readable): Promise { 9 | const buffers: Uint8Array[] | Buffer[] = []; 10 | for await (const chunk of readable) { 11 | buffers.push(chunk); 12 | } 13 | return Buffer.concat(buffers).toString(); 14 | } 15 | 16 | describe('$dara form', function () { 17 | 18 | it('getBoundary should ok', function () { 19 | assert.ok($dara.Form.getBoundary().length > 10); 20 | }); 21 | 22 | it('toFileForm should ok', async function () { 23 | const result = await read($dara.Form.toFileForm({ 24 | stringkey: 'string' 25 | }, 'boundary')); 26 | assert.deepStrictEqual(result, '--boundary\r\n' 27 | + 'Content-Disposition: form-data; name="stringkey"\r\n\r\n' 28 | + 'string\r\n' 29 | + '\r\n' 30 | + '--boundary--\r\n'); 31 | }); 32 | 33 | it('file field should ok', async function () { 34 | const fileStream = createReadStream(join(__dirname, './fixtures/test.json')); 35 | const result = await read($dara.Form.toFileForm({ 36 | stringkey: 'string', 37 | filefield: new $dara.FileField({ 38 | filename: 'fakefilename', 39 | contentType: 'application/json', 40 | content: fileStream 41 | }), 42 | }, 'boundary')); 43 | assert.deepStrictEqual(result, '--boundary\r\n' 44 | + 'Content-Disposition: form-data; name="stringkey"\r\n\r\n' 45 | + 'string\r\n' 46 | + '--boundary\r\n' 47 | + 'Content-Disposition: form-data; name="filefield"; filename="fakefilename"\r\n' 48 | + 'Content-Type: application/json\r\n' 49 | + '\r\n' 50 | + '{"key":"value"}' 51 | + '\r\n' 52 | + '--boundary--\r\n'); 53 | }); 54 | 55 | it('toFormString should ok', function () { 56 | assert.deepStrictEqual($dara.Form.toFormString({}), ''); 57 | assert.deepStrictEqual($dara.Form.toFormString({ a: 'b c d' }), 'a=b%20c%20d'); 58 | }); 59 | }); -------------------------------------------------------------------------------- /test/error.spec.ts: -------------------------------------------------------------------------------- 1 | import * as $dara from '../src/index'; 2 | import 'mocha'; 3 | import assert from 'assert'; 4 | 5 | describe('$dara error', function () { 6 | it('init Base Error should be okay', () => { 7 | const err = new $dara.BaseError({ 8 | code: 'Error', 9 | message: 'Test Error Message' 10 | }); 11 | assert.strictEqual(err.name, 'BaseError'); 12 | assert.strictEqual(err.code, 'Error'); 13 | assert.strictEqual(err.message, 'Error: Test Error Message'); 14 | }); 15 | 16 | it('retryError should ok', function () { 17 | const err = $dara.retryError(new $dara.Request(), null); 18 | assert.strictEqual(err.name, 'RetryError'); 19 | }); 20 | 21 | it('newUnretryableError should ok', function () { 22 | const err = $dara.newUnretryableError(new $dara.RetryPolicyContext({})); 23 | assert.strictEqual(err.name, 'UnretryableError'); 24 | }); 25 | 26 | it('newError should ok', function () { 27 | let err = $dara.newError({ 28 | code: 'code', 29 | message: 'message' 30 | }); 31 | assert.strictEqual(err.message, 'code: message'); 32 | assert.strictEqual(err.code, 'code'); 33 | assert.ok(err.statusCode === undefined); 34 | assert.ok(err.data === undefined); 35 | err = $dara.newError({ 36 | code: 'code', 37 | message: 'message', 38 | data: { 39 | statusCode: 200, 40 | description: 'description' 41 | }, 42 | description: 'error description', 43 | accessDeniedDetail: { 44 | 'AuthAction': 'ram:ListUsers', 45 | 'AuthPrincipalType': 'SubUser', 46 | 'PolicyType': 'ResourceGroupLevelIdentityBassdPolicy', 47 | 'NoPermissionType': 'ImplicitDeny' 48 | } 49 | }); 50 | assert.strictEqual(err.message, 'code: message'); 51 | assert.strictEqual(err.code, 'code'); 52 | assert.strictEqual(err.statusCode, 200); 53 | assert.strictEqual(err.data.statusCode, 200); 54 | assert.strictEqual(err.data.description, 'description'); 55 | assert.strictEqual(err.description, 'error description'); 56 | assert.ok(typeof err.accessDeniedDetail === 'object'); 57 | assert.strictEqual(err.accessDeniedDetail.NoPermissionType, 'ImplicitDeny'); 58 | }); 59 | }); -------------------------------------------------------------------------------- /src/form.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | import { stringify } from 'querystring'; 3 | 4 | export class FileFormStream extends Readable { 5 | form: { [key: string]: any }; 6 | boundary: string; 7 | keys: string[]; 8 | index: number; 9 | streaming: boolean; 10 | 11 | constructor(form: { [key: string]: any }, boundary: string) { 12 | super(); 13 | this.form = form; 14 | this.keys = Object.keys(form); 15 | this.index = 0; 16 | this.boundary = boundary; 17 | this.streaming = false; 18 | } 19 | 20 | _read() { 21 | if (this.streaming) { 22 | return; 23 | } 24 | 25 | const separator = this.boundary; 26 | if (this.index < this.keys.length) { 27 | const name = this.keys[this.index]; 28 | const fieldValue = this.form[name]; 29 | if (typeof fieldValue.filename === 'string' && 30 | typeof fieldValue.contentType === 'string' && 31 | fieldValue.content instanceof Readable) { 32 | const body = 33 | `--${separator}\r\n` + 34 | `Content-Disposition: form-data; name="${name}"; filename="${fieldValue.filename}"\r\n` + 35 | `Content-Type: ${fieldValue.contentType}\r\n\r\n`; 36 | this.push(Buffer.from(body)); 37 | this.streaming = true; 38 | fieldValue.content.on('data', (chunk: any) => { 39 | this.push(chunk); 40 | }); 41 | fieldValue.content.on('end', () => { 42 | this.index++; 43 | this.streaming = false; 44 | this.push(''); 45 | }); 46 | } else { 47 | this.push(Buffer.from(`--${separator}\r\n` + 48 | `Content-Disposition: form-data; name="${name}"\r\n\r\n` + 49 | `${fieldValue}\r\n`)); 50 | this.index++; 51 | } 52 | } else { 53 | this.push(Buffer.from(`\r\n--${separator}--\r\n`)); 54 | this.push(null); 55 | } 56 | } 57 | } 58 | 59 | export default class Form { 60 | 61 | static getBoundary(): string { 62 | return 'boundary' + Math.random().toString(16).slice(-12); 63 | } 64 | 65 | static toFileForm(form: { [key: string]: any }, boundary: string): Readable { 66 | return new FileFormStream(form, boundary); 67 | } 68 | 69 | static toFormString(data: { [key: string]: any }) { 70 | return stringify(data); 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/file.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as util from 'util'; 3 | import { Readable, Writable } from 'stream'; 4 | import TeaDate from './date'; 5 | 6 | const exists = util.promisify(fs.exists); 7 | const stat = util.promisify(fs.stat); 8 | const read = util.promisify(fs.read); 9 | const write = util.promisify(fs.write); 10 | const open = util.promisify(fs.open); 11 | const close = util.promisify(fs.close); 12 | export default class TeaFile { 13 | _path: string 14 | _stat: fs.Stats 15 | _fd: number 16 | _position: number 17 | 18 | constructor(path: string) { 19 | this._path = path; 20 | this._position = 0; 21 | } 22 | 23 | path(): string{ 24 | return this._path; 25 | } 26 | 27 | async createTime(): Promise{ 28 | if(!this._stat) { 29 | this._stat = await stat(this._path); 30 | } 31 | return new TeaDate(this._stat.birthtime); 32 | } 33 | 34 | async modifyTime(): Promise{ 35 | if(!this._stat) { 36 | this._stat = await stat(this._path); 37 | } 38 | return new TeaDate(this._stat.mtime); 39 | } 40 | 41 | async length(): Promise{ 42 | if(!this._stat) { 43 | this._stat = await stat(this._path); 44 | } 45 | return this._stat.size; 46 | } 47 | 48 | async read(size: number): Promise { 49 | if(!this._fd) { 50 | this._fd = await open(this._path, 'a+'); 51 | } 52 | const buf = Buffer.alloc(size); 53 | const { bytesRead, buffer } = await read(this._fd, buf, 0, size, this._position); 54 | if(!bytesRead) { 55 | return null; 56 | } 57 | this._position += bytesRead; 58 | return buffer; 59 | } 60 | 61 | async write(data: Buffer): Promise { 62 | if(!this._fd) { 63 | this._fd = await open(this._path, 'a+'); 64 | } 65 | 66 | await write(this._fd, data); 67 | 68 | this._stat = await stat(this._path); 69 | return; 70 | } 71 | 72 | async close(): Promise { 73 | if(!this._fd) { 74 | return; 75 | } 76 | await close(this._fd); 77 | return; 78 | } 79 | 80 | static async exists(path: string): Promise { 81 | return await exists(path); 82 | } 83 | 84 | static createReadStream(path: string): Readable { 85 | return fs.createReadStream(path); 86 | } 87 | 88 | static createWriteStream(path: string): Writable { 89 | return fs.createWriteStream(path); 90 | } 91 | } -------------------------------------------------------------------------------- /src/date.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export default class TeaDate { 4 | date: moment.Moment 5 | 6 | constructor(date: moment.MomentInput) { 7 | this.date = moment(date); 8 | } 9 | 10 | format(layout: string): string { 11 | layout = layout.replace(/y/g, 'Y') 12 | .replace(/d/g, 'D').replace(/h/g, 'H') 13 | .replace(/a/g, 'A').replace(/E/g, 'd'); 14 | return this.date.format(layout); 15 | } 16 | 17 | unix(): number { 18 | return this.date.unix(); 19 | } 20 | 21 | sub(amount: moment.unitOfTime.Base, unit: number): TeaDate { 22 | const date = moment(this.date).subtract(unit, amount); 23 | return new TeaDate(date); 24 | } 25 | 26 | add(amount: moment.unitOfTime.Base, unit: number): TeaDate { 27 | const date = moment(this.date).add(unit, amount); 28 | return new TeaDate(date); 29 | } 30 | 31 | diff(amount: moment.unitOfTime.Base, diffDate: TeaDate): number { 32 | return this.date.diff(diffDate.date, amount); 33 | } 34 | 35 | hour(): number { 36 | return this.date.hour(); 37 | } 38 | 39 | minute(): number { 40 | return this.date.minute(); 41 | } 42 | 43 | second(): number { 44 | return this.date.second(); 45 | } 46 | 47 | month(): number { 48 | return this.date.month() + 1; 49 | } 50 | 51 | year(): number { 52 | return this.date.year(); 53 | } 54 | 55 | dayOfMonth(): number { 56 | return this.date.date(); 57 | } 58 | 59 | dayOfWeek(): number { 60 | const weekday = this.date.weekday(); 61 | if(weekday === 0) { 62 | // sunday 63 | return 7; 64 | } 65 | return weekday + 1; 66 | } 67 | 68 | weekOfMonth(): number { 69 | const startWeek = moment(this.date).startOf('month').week(); 70 | let dateWeek = this.date.week(); 71 | if (this.date.weekday() === 0) { 72 | dateWeek = dateWeek - 1; 73 | } 74 | if (dateWeek === 0 && this.date.date() > 1) { 75 | // the last day of this year is sunday 76 | return this.sub('day', 1).weekOfMonth(); 77 | } 78 | const monthWeek = dateWeek - startWeek; 79 | if(monthWeek < 0) { 80 | // start of a new year 81 | return 1; 82 | } 83 | return monthWeek + 1; 84 | } 85 | 86 | weekOfYear(): number { 87 | const weekday = this.date.weekday(); 88 | const week = this.date.week(); 89 | if(weekday === 0 && week === 1 && this.date.date() > 1) { 90 | // the last day of this year is sunday 91 | return this.sub('day', 1).weekOfYear(); 92 | } 93 | return this.date.week(); 94 | } 95 | } -------------------------------------------------------------------------------- /src/xml.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated, don't edit it 2 | import { Parser, Builder } from 'xml2js'; 3 | 4 | export default class TeaXML { 5 | 6 | static parseXml(body: string, response: T): { [key: string]: any } { 7 | let ret: { [key: string]: any } = this._parseXML(body); 8 | if (response !== null && typeof response !== 'undefined') { 9 | ret = this._xmlCast(ret, response); 10 | } 11 | return ret; 12 | } 13 | 14 | static toXML(body: { [key: string]: any }): string { 15 | const builder = new Builder(); 16 | return builder.buildObject(body); 17 | } 18 | 19 | static _parseXML(body: string): any { 20 | const parser = new Parser({ explicitArray: false }); 21 | const result: { [key: string]: any } = {}; 22 | parser.parseString(body, function (err: any, output: any) { 23 | result.err = err; 24 | result.output = output; 25 | }); 26 | if (result.err) { 27 | throw result.err; 28 | } 29 | 30 | return result.output; 31 | } 32 | 33 | static _xmlCast(obj: any, clazz: T): { [key: string]: any } { 34 | obj = obj || {}; 35 | const ret: { [key: string]: any } = {}; 36 | const clz = clazz as any; 37 | const names: { [key: string]: string } = clz.names(); 38 | const types: { [key: string]: any } = clz.types(); 39 | 40 | Object.keys(names).forEach((key) => { 41 | const originName = names[key]; 42 | let value = obj[originName]; 43 | const type = types[key]; 44 | switch (type) { 45 | case 'boolean': 46 | if (!value) { 47 | ret[originName] = false; 48 | return; 49 | } 50 | ret[originName] = value === 'false' ? false : true; 51 | return; 52 | case 'number': 53 | if (value != 0 && !value) { 54 | ret[originName] = NaN; 55 | return; 56 | } 57 | ret[originName] = +value; 58 | return; 59 | case 'string': 60 | if (!value) { 61 | ret[originName] = ''; 62 | return; 63 | } 64 | ret[originName] = value.toString(); 65 | return; 66 | default: 67 | if (type.type === 'array') { 68 | if (!value) { 69 | ret[originName] = []; 70 | return; 71 | } 72 | if (!Array.isArray(value)) { 73 | value = [value]; 74 | } 75 | if (typeof type.itemType === 'function') { 76 | ret[originName] = value.map((d: any) => { 77 | return this._xmlCast(d, type.itemType); 78 | }); 79 | } else { 80 | ret[originName] = value; 81 | } 82 | } else if (typeof type === 'function') { 83 | if (!value) { 84 | value = {} 85 | } 86 | ret[originName] = this._xmlCast(value, type); 87 | } else { 88 | ret[originName] = value; 89 | } 90 | } 91 | }) 92 | return ret; 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /test/file.spec.ts: -------------------------------------------------------------------------------- 1 | import * as $dara from '../src/index'; 2 | import 'mocha'; 3 | import path from 'path'; 4 | import assert from 'assert'; 5 | import * as fs from 'fs'; 6 | import moment from 'moment'; 7 | 8 | describe('$dara file', function () { 9 | const file = new $dara.File(path.join(__dirname, './fixtures/test.txt')); 10 | fs.writeFileSync(path.join(__dirname, './fixtures/test.txt'), 'Test For File', 'utf8'); 11 | const stat = fs.statSync(path.join(__dirname, './fixtures/test.txt')); 12 | it('path should be ok', () => { 13 | assert.strictEqual(file.path(), path.join(__dirname, './fixtures/test.txt')); 14 | }); 15 | 16 | it('createTime should ok', async () => { 17 | const createTime = await file.createTime(); 18 | assert.strictEqual(createTime.format('YYYY-MM-DD HH:mm:ss'), moment(stat.birthtime).format('YYYY-MM-DD HH:mm:ss')); 19 | const newFile = new $dara.File(path.join(__dirname, './fixtures/test.txt')); 20 | const newCreateTime = await newFile.createTime(); 21 | assert.strictEqual(newCreateTime.format('YYYY-MM-DD HH:mm:ss'), moment(stat.birthtime).format('YYYY-MM-DD HH:mm:ss')); 22 | }); 23 | 24 | it('modifyTime should ok', async () => { 25 | const modifyTime = await file.modifyTime(); 26 | assert.strictEqual(modifyTime.format('YYYY-MM-DD HH:mm:ss'), moment(stat.mtime).format('YYYY-MM-DD HH:mm:ss')); 27 | const newFile = new $dara.File(path.join(__dirname, './fixtures/test.txt')); 28 | const newModifyTime = await newFile.modifyTime(); 29 | assert.strictEqual(newModifyTime.format('YYYY-MM-DD HH:mm:ss'), moment(stat.mtime).format('YYYY-MM-DD HH:mm:ss')); 30 | }); 31 | 32 | it('length should ok', async () => { 33 | assert.strictEqual(await file.length(), stat.size); 34 | const newFile = new $dara.File(path.join(__dirname, './fixtures/test.txt')); 35 | assert.strictEqual(await newFile.length(), stat.size); 36 | }); 37 | 38 | it('read should ok', async () => { 39 | const text1 = await file.read(4); 40 | assert.strictEqual(text1.toString(), 'Test'); 41 | const text2 = await file.read(4); 42 | assert.strictEqual(text2.toString(), ' For'); 43 | assert.strictEqual(file._position, 8); 44 | const emptyFile = new $dara.File(path.join(__dirname, './fixtures/empty.txt')); 45 | const empty = await emptyFile.read(10); 46 | assert.strictEqual(empty, null); 47 | await emptyFile.close(); 48 | }); 49 | 50 | 51 | it('write should ok', async () => { 52 | await file.write(Buffer.from(' Test')); 53 | const modifyTime = await file.modifyTime(); 54 | const length = await file.length(); 55 | assert.strictEqual(modifyTime.format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD HH:mm:ss')); 56 | assert.strictEqual(length, stat.size + 5); 57 | await file.close(); 58 | const newFile = new $dara.File(path.join(__dirname, './fixtures/newfile.txt')); 59 | await newFile.write(Buffer.from('Test')); 60 | const text = await newFile.read(4); 61 | assert.strictEqual(text.toString(), 'Test'); 62 | await newFile.close(); 63 | }); 64 | 65 | it('exists should ok', async () => { 66 | assert.ok(await $dara.File.exists(path.join(__dirname, './fixtures/test.txt'))); 67 | assert.ok(!(await $dara.File.exists(path.join(__dirname, './fixtures/test1.txt')))); 68 | }); 69 | 70 | it('creatStream should ok', function () { 71 | const rs = $dara.File.createReadStream(path.join(__dirname, './fixtures/test.txt')); 72 | const ws = $dara.File.createWriteStream(path.join(__dirname, './fixtures/test.txt')); 73 | rs.pipe(ws); 74 | }); 75 | 76 | }); -------------------------------------------------------------------------------- /src/stream.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | 3 | const DATA_PREFIX = 'data:'; 4 | const EVENT_PREFIX = 'event:'; 5 | const ID_PREFIX = 'id:'; 6 | const RETRY_PREFIX = 'retry:'; 7 | 8 | function isDigitsOnly(str: string) { 9 | for (let i = 0; i < str.length; i++) { 10 | const c = str.charAt(i); 11 | if (c < '0' || c > '9') { 12 | return false; 13 | } 14 | } 15 | return str.length > 0; 16 | } 17 | 18 | export class SSEEvent { 19 | data?: string; 20 | id?: string; 21 | event?: string; 22 | retry?: number; 23 | 24 | constructor(data: { [key: string]: any } = {}) { 25 | this.data = data.data; 26 | this.id = data.id; 27 | this.event = data.event; 28 | this.retry = data.retry; 29 | } 30 | } 31 | 32 | 33 | function read(readable: Readable): Promise { 34 | return new Promise((resolve, reject) => { 35 | let onData: { (chunk: any): void; (buf: Buffer): void; (chunk: any): void; }, 36 | onError: { (err: Error): void; (err: Error): void; (err: Error): void; }, 37 | onEnd: { (): void; (): void; (): void; }; 38 | const cleanup = function () { 39 | // cleanup 40 | readable.removeListener('error', onError); 41 | readable.removeListener('data', onData); 42 | readable.removeListener('end', onEnd); 43 | }; 44 | 45 | const bufs: Uint8Array[] | Buffer[] = []; 46 | let size = 0; 47 | 48 | onData = function (buf: Buffer) { 49 | bufs.push(buf); 50 | size += buf.length; 51 | }; 52 | 53 | onError = function (err: Error) { 54 | cleanup(); 55 | reject(err); 56 | }; 57 | 58 | onEnd = function () { 59 | cleanup(); 60 | resolve(Buffer.concat(bufs, size)); 61 | }; 62 | 63 | readable.on('error', onError); 64 | readable.on('data', onData); 65 | readable.on('end', onEnd); 66 | }); 67 | } 68 | 69 | 70 | 71 | function readyToRead(readable: Readable) { 72 | return new Promise((resolve, reject) => { 73 | let onReadable: { (): void; (): void; (): void; }, 74 | onEnd: { (): void; (): void; (): void; }, 75 | onError: { (err: Error): void; (err: any): void; (err: Error): void; }; 76 | const cleanup = function () { 77 | // cleanup 78 | readable.removeListener('error', onError); 79 | readable.removeListener('end', onEnd); 80 | readable.removeListener('readable', onReadable); 81 | }; 82 | 83 | onReadable = function () { 84 | cleanup(); 85 | resolve(false); 86 | }; 87 | 88 | onEnd = function () { 89 | cleanup(); 90 | resolve(true); 91 | }; 92 | 93 | onError = function (err) { 94 | cleanup(); 95 | reject(err); 96 | }; 97 | 98 | readable.once('readable', onReadable); 99 | readable.once('end', onEnd); 100 | readable.once('error', onError); 101 | }); 102 | } 103 | 104 | interface EventResult { 105 | events: SSEEvent[]; 106 | remain: string; 107 | } 108 | 109 | function tryGetEvents(head: string, chunk: string): EventResult { 110 | const all = head + chunk; 111 | let start = 0; 112 | const events = []; 113 | for (let i = 0; i < all.length - 1; i++) { 114 | const c = all[i]; 115 | const c2 = all[i + 1]; 116 | if (c === '\n' && c2 === '\n') { 117 | const part = all.substring(start, i); 118 | const lines = part.split('\n'); 119 | const event = new SSEEvent(); 120 | lines.forEach((line: string) => { 121 | if (line.startsWith(DATA_PREFIX)) { 122 | event.data = line.substring(DATA_PREFIX.length).trim(); 123 | } else if (line.startsWith(EVENT_PREFIX)) { 124 | event.event = line.substring(EVENT_PREFIX.length).trim(); 125 | } else if (line.startsWith(ID_PREFIX)) { 126 | event.id = line.substring(ID_PREFIX.length).trim(); 127 | } else if (line.startsWith(RETRY_PREFIX)) { 128 | const retry = line.substring(RETRY_PREFIX.length).trim(); 129 | if (isDigitsOnly(retry)) { 130 | event.retry = parseInt(retry, 10); 131 | } 132 | } else if (line.startsWith(':')) { 133 | // ignore the line 134 | } 135 | }); 136 | events.push(event); 137 | start = i + 2; 138 | } 139 | } 140 | 141 | const remain = all.substring(start); 142 | return { events, remain }; 143 | } 144 | 145 | 146 | export default class TeaStream { 147 | 148 | static async readAsBytes(stream: Readable): Promise { 149 | return await read(stream); 150 | } 151 | 152 | static async readAsString(stream: Readable): Promise { 153 | const buff = await TeaStream.readAsBytes(stream); 154 | return buff.toString(); 155 | } 156 | 157 | static async readAsJSON(stream: Readable): Promise { 158 | const str = await TeaStream.readAsString(stream); 159 | return JSON.parse(str); 160 | } 161 | 162 | static async *readAsSSE(stream: Readable): AsyncGenerator { 163 | let rest = ''; 164 | while (true) { 165 | const ended = await readyToRead(stream); 166 | if (ended) { 167 | return; 168 | } 169 | 170 | let chunk; 171 | while (null !== (chunk = stream.read())) { 172 | const { events, remain } = tryGetEvents(rest, chunk.toString()); 173 | rest = remain; 174 | if (events && events.length > 0) { 175 | for (const event of events) { 176 | yield event; 177 | } 178 | } 179 | } 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /src/retry.ts: -------------------------------------------------------------------------------- 1 | import * as $core from './core'; 2 | import * as $error from './error'; 3 | const MAX_DELAY_TIME = 120 * 1000; 4 | const MIN_DELAY_TIME = 100; 5 | export class BackoffPolicy{ 6 | policy: string; 7 | constructor(option: {[key: string]: any}) { 8 | this.policy = option.policy; 9 | } 10 | 11 | getDelayTime(ctx: RetryPolicyContext): number{ 12 | throw Error('un-implement'); 13 | } 14 | 15 | static newBackoffPolicy(option: {[key: string]: any}): BackoffPolicy { 16 | switch(option.policy) { 17 | case 'Fixed': 18 | return new FixedBackoffPolicy(option); 19 | case 'Random': 20 | return new RandomBackoffPolicy(option); 21 | case 'Exponential': 22 | return new ExponentialBackoffPolicy(option); 23 | case 'EqualJitter': 24 | case 'ExponentialWithEqualJitter': 25 | return new EqualJitterBackoffPolicy(option); 26 | case 'FullJitter': 27 | case 'ExponentialWithFullJitter': 28 | return new FullJitterBackoffPolicy(option); 29 | } 30 | } 31 | } 32 | 33 | 34 | class FixedBackoffPolicy extends BackoffPolicy { 35 | period: number; 36 | constructor(option: {[key: string]: any}) { 37 | super(option); 38 | this.period = option.period; 39 | } 40 | 41 | getDelayTime(ctx: RetryPolicyContext): number{ 42 | return this.period; 43 | } 44 | } 45 | 46 | class RandomBackoffPolicy extends BackoffPolicy { 47 | period: number; 48 | cap: number; 49 | constructor(option: {[key: string]: any}) { 50 | super(option); 51 | this.period = option.period; 52 | this.cap = option.cap || 20 * 1000; 53 | } 54 | 55 | getDelayTime(ctx: RetryPolicyContext): number{ 56 | const randomTime = Math.floor(Math.random() * (ctx.retriesAttempted * this.period)); 57 | if(randomTime > this.cap) { 58 | return this.cap; 59 | } 60 | return randomTime; 61 | } 62 | } 63 | 64 | class ExponentialBackoffPolicy extends BackoffPolicy { 65 | period: number; 66 | cap: number; 67 | constructor(option: {[key: string]: any}) { 68 | super(option); 69 | this.period = option.period; 70 | //default value: 3 days 71 | this.cap = option.cap || 3 * 24 * 60 * 60 * 1000; 72 | } 73 | 74 | getDelayTime(ctx: RetryPolicyContext): number{ 75 | const randomTime = Math.pow(2, ctx.retriesAttempted * this.period); 76 | if(randomTime > this.cap) { 77 | return this.cap; 78 | } 79 | return randomTime; 80 | } 81 | } 82 | 83 | class EqualJitterBackoffPolicy extends BackoffPolicy { 84 | period: number; 85 | cap: number; 86 | constructor(option: {[key: string]: any}) { 87 | super(option); 88 | this.period = option.period; 89 | //default value: 3 days 90 | this.cap = option.cap || 3 * 24 * 60 * 60 * 1000; 91 | } 92 | 93 | getDelayTime(ctx: RetryPolicyContext): number{ 94 | const ceil = Math.min(this.cap, Math.pow(2, ctx.retriesAttempted * this.period)); 95 | return ceil / 2 + Math.floor(Math.random() * (ceil / 2 + 1)); 96 | } 97 | } 98 | 99 | class FullJitterBackoffPolicy extends BackoffPolicy { 100 | period: number; 101 | cap: number; 102 | constructor(option: {[key: string]: any}) { 103 | super(option); 104 | this.period = option.period; 105 | //default value: 3 days 106 | this.cap = option.cap || 3 * 24 * 60 * 60 * 1000; 107 | } 108 | 109 | getDelayTime(ctx: RetryPolicyContext): number{ 110 | const ceil = Math.min(this.cap, Math.pow(2, ctx.retriesAttempted * this.period)); 111 | return Math.floor(Math.random() * ceil); 112 | } 113 | } 114 | 115 | 116 | export class RetryCondition { 117 | maxAttempts: number; 118 | backoff: BackoffPolicy; 119 | exception: string[]; 120 | errorCode: string[]; 121 | maxDelay: number; 122 | constructor(condition: {[key: string]: any}) { 123 | this.maxAttempts = condition.maxAttempts; 124 | this.backoff = condition.backoff && BackoffPolicy.newBackoffPolicy(condition.backoff); 125 | this.exception = condition.exception; 126 | this.errorCode = condition.errorCode; 127 | this.maxDelay = condition.maxDelay; 128 | } 129 | } 130 | 131 | 132 | export class RetryOptions { 133 | retryable: boolean; 134 | retryCondition: RetryCondition[]; 135 | noRetryCondition: RetryCondition[]; 136 | constructor(options: {[key: string]: any}) { 137 | this.retryable = options.retryable; 138 | this.retryCondition = (options.retryCondition || []).map((condition: { [key: string]: any; }) => { 139 | return new RetryCondition(condition); 140 | }); 141 | 142 | this.noRetryCondition = (options.noRetryCondition || []).map((condition: { [key: string]: any; }) => { 143 | return new RetryCondition(condition); 144 | }); 145 | } 146 | } 147 | 148 | export class RetryPolicyContext { 149 | key: string; 150 | retriesAttempted: number; 151 | httpRequest: $core.Request; 152 | httpResponse: $core.Response; 153 | exception: $error.ResponseError | $error.BaseError; 154 | constructor(options: {[key: string]: any}) { 155 | this.key = options.key; 156 | this.retriesAttempted = options.retriesAttempted || 0; 157 | this.httpRequest = options.httpRequest || null; 158 | this.httpResponse = options.httpResponse || null; 159 | this.exception = options.exception || null; 160 | } 161 | } 162 | 163 | export function shouldRetry(options: RetryOptions, ctx: RetryPolicyContext): boolean { 164 | if(ctx.retriesAttempted === 0) { 165 | return true; 166 | } 167 | if(!options || !options.retryable) { 168 | return false; 169 | } 170 | const retriesAttempted = ctx.retriesAttempted; 171 | const ex = ctx.exception; 172 | let conditions = options.noRetryCondition; 173 | for(let i = 0; i < conditions.length; i++) { 174 | const condition = conditions[i]; 175 | if(condition.exception.includes(ex.name) || condition.errorCode.includes(ex.code)) { 176 | return false; 177 | } 178 | } 179 | conditions = options.retryCondition; 180 | for(let i = 0; i < conditions.length; i++) { 181 | const condition = conditions[i]; 182 | if(!condition.exception.includes(ex.name) && !condition.errorCode.includes(ex.code)) { 183 | continue; 184 | } 185 | if(retriesAttempted >= condition.maxAttempts) { 186 | return false; 187 | } 188 | return true; 189 | } 190 | return false; 191 | } 192 | 193 | export function getBackoffDelay(options: RetryOptions, ctx: RetryPolicyContext): number { 194 | const ex = ctx.exception; 195 | const conditions = options.retryCondition; 196 | for(let i = 0; i < conditions.length; i++) { 197 | const condition = conditions[i]; 198 | if(!condition.exception.includes(ex.name) && !condition.errorCode.includes(ex.code)) { 199 | continue; 200 | } 201 | const maxDelay = condition.maxDelay || MAX_DELAY_TIME; 202 | const retryAfter = (ctx.exception as $error.ResponseError).retryAfter; 203 | if(retryAfter !== undefined) { 204 | return Math.min(retryAfter, maxDelay); 205 | } 206 | 207 | if(!condition.backoff) { 208 | return MIN_DELAY_TIME; 209 | } 210 | return Math.min(condition.backoff.getDelayTime(ctx), maxDelay); 211 | } 212 | return MIN_DELAY_TIME; 213 | } -------------------------------------------------------------------------------- /test/stream.spec.ts: -------------------------------------------------------------------------------- 1 | import * as $dara from '../src/index'; 2 | import 'mocha'; 3 | import assert from 'assert'; 4 | import { Readable } from 'stream'; 5 | import * as http from 'http'; 6 | import * as httpx from 'httpx'; 7 | import { SSEEvent } from '../src/stream'; 8 | 9 | 10 | const server = http.createServer((req, res) => { 11 | const headers = { 12 | 'Content-Type': 'text/event-stream', 13 | 'Connection': 'keep-alive', 14 | 'Cache-Control': 'no-cache' 15 | }; 16 | res.writeHead(200, headers); 17 | res.flushHeaders(); 18 | let count = 0; 19 | if (req.url === '/sse') { 20 | const timer = setInterval(() => { 21 | if (count >= 5) { 22 | clearInterval(timer); 23 | res.end(); 24 | return; 25 | } 26 | res.write(`data: ${JSON.stringify({ count: count })}\nevent: flow\nid: sse-test\nretry: 3\n:heartbeat\n\n`); 27 | count++; 28 | }, 100); 29 | } else if (req.url === '/sse_with_no_spaces') { 30 | const timer = setInterval(() => { 31 | if (count >= 5) { 32 | clearInterval(timer); 33 | res.end(); 34 | return; 35 | } 36 | res.write(`data:${JSON.stringify({ count: count })}\nevent:flow\nid:sse-test\nretry:3\n\n`); 37 | count++; 38 | }, 100); 39 | } else if (req.url === '/sse_invalid_retry') { 40 | const timer = setInterval(() => { 41 | if (count >= 5) { 42 | clearInterval(timer); 43 | res.end(); 44 | return; 45 | } 46 | res.write(`data:${JSON.stringify({ count: count })}\nevent:flow\nid:sse-test\nretry: abc\n\n`); 47 | count++; 48 | }, 100); 49 | } else if (req.url === '/sse_with_data_divided') { 50 | const timer = setInterval(() => { 51 | if (count >= 5) { 52 | clearInterval(timer); 53 | res.end(); 54 | return; 55 | } 56 | if (count === 1) { 57 | res.write('data:{"count":'); 58 | count++; 59 | return; 60 | } 61 | if (count === 2) { 62 | res.write(`${count++},"tag":"divided"}\nevent:flow\nid:sse-test\nretry:3\n\n`); 63 | return; 64 | } 65 | res.write(`data:${JSON.stringify({ count: count++ })}\nevent:flow\nid:sse-test\nretry:3\n\n`); 66 | }, 100); 67 | } 68 | }); 69 | 70 | class MyReadable extends Readable { 71 | value: Buffer 72 | 73 | constructor(value: Buffer) { 74 | super(); 75 | this.value = value; 76 | } 77 | 78 | _read() { 79 | this.push(this.value); 80 | this.push(null); 81 | } 82 | } 83 | 84 | describe('$dara stream', function () { 85 | 86 | before((done) => { 87 | server.listen(8384, done); 88 | }); 89 | 90 | after(function (done) { 91 | this.timeout(20000); 92 | server.close(done); 93 | }); 94 | 95 | it('readAsJSON', async function () { 96 | const readable = new MyReadable(Buffer.from(JSON.stringify({ 'a': 'b' }))); 97 | const result = await $dara.Stream.readAsJSON(readable); 98 | assert.deepStrictEqual(result, { 'a': 'b' }); 99 | }); 100 | 101 | it('readAsBytes', async function () { 102 | const readable = new MyReadable(Buffer.from(JSON.stringify({ 'a': 'b' }))); 103 | const result = await $dara.Stream.readAsBytes(readable); 104 | assert.deepStrictEqual(result, Buffer.from('{"a":"b"}')); 105 | }); 106 | 107 | it('readAsString', async function () { 108 | const readable = new MyReadable(Buffer.from(JSON.stringify({ 'a': 'b' }))); 109 | const result = await $dara.Stream.readAsString(readable); 110 | assert.deepStrictEqual(result, '{"a":"b"}'); 111 | }); 112 | 113 | it('readAsSSE', async function () { 114 | const res = await httpx.request("http://127.0.0.1:8384/sse", { readTimeout: 5000 }); 115 | assert.strictEqual(res.statusCode, 200); 116 | const events: SSEEvent[] = []; 117 | 118 | for await (const event of $dara.Stream.readAsSSE(res)) { 119 | 120 | events.push(event); 121 | } 122 | assert.strictEqual(events.length, 5); 123 | 124 | assert.deepStrictEqual([new SSEEvent({ 125 | data: '{"count":0}', 126 | event: 'flow', 127 | id: 'sse-test', 128 | retry: 3, 129 | }), new SSEEvent({ 130 | data: '{"count":1}', 131 | event: 'flow', 132 | id: 'sse-test', 133 | retry: 3, 134 | }), new SSEEvent({ 135 | data: '{"count":2}', 136 | event: 'flow', 137 | id: 'sse-test', 138 | retry: 3, 139 | }), new SSEEvent({ 140 | data: '{"count":3}', 141 | event: 'flow', 142 | id: 'sse-test', 143 | retry: 3, 144 | }), new SSEEvent({ 145 | data: '{"count":4}', 146 | event: 'flow', 147 | id: 'sse-test', 148 | retry: 3, 149 | })], events); 150 | }); 151 | 152 | it('readAsSSE with no spaces', async function () { 153 | const res = await httpx.request("http://127.0.0.1:8384/sse_with_no_spaces", { readTimeout: 5000 }); 154 | assert.strictEqual(res.statusCode, 200); 155 | const events: SSEEvent[] = []; 156 | 157 | for await (const event of $dara.Stream.readAsSSE(res)) { 158 | 159 | events.push(event); 160 | } 161 | assert.strictEqual(events.length, 5); 162 | 163 | assert.deepStrictEqual([new SSEEvent({ 164 | data: '{"count":0}', 165 | event: 'flow', 166 | id: 'sse-test', 167 | retry: 3, 168 | }), new SSEEvent({ 169 | data: '{"count":1}', 170 | event: 'flow', 171 | id: 'sse-test', 172 | retry: 3, 173 | }), new SSEEvent({ 174 | data: '{"count":2}', 175 | event: 'flow', 176 | id: 'sse-test', 177 | retry: 3, 178 | }), new SSEEvent({ 179 | data: '{"count":3}', 180 | event: 'flow', 181 | id: 'sse-test', 182 | retry: 3, 183 | }), new SSEEvent({ 184 | data: '{"count":4}', 185 | event: 'flow', 186 | id: 'sse-test', 187 | retry: 3, 188 | })], events); 189 | }); 190 | 191 | it('readAsSSE with invalid retry', async function () { 192 | const res = await httpx.request("http://127.0.0.1:8384/sse_invalid_retry", { readTimeout: 5000 }); 193 | assert.strictEqual(res.statusCode, 200); 194 | const events: SSEEvent[] = []; 195 | 196 | for await (const event of $dara.Stream.readAsSSE(res)) { 197 | 198 | events.push(event); 199 | } 200 | assert.strictEqual(events.length, 5); 201 | 202 | assert.deepStrictEqual([new SSEEvent({ 203 | data: '{"count":0}', 204 | event: 'flow', 205 | id: 'sse-test', 206 | retry: undefined, 207 | }), new SSEEvent({ 208 | data: '{"count":1}', 209 | event: 'flow', 210 | id: 'sse-test', 211 | retry: undefined, 212 | }), new SSEEvent({ 213 | data: '{"count":2}', 214 | event: 'flow', 215 | id: 'sse-test', 216 | retry: undefined, 217 | }), new SSEEvent({ 218 | data: '{"count":3}', 219 | event: 'flow', 220 | id: 'sse-test', 221 | retry: undefined, 222 | }), new SSEEvent({ 223 | data: '{"count":4}', 224 | event: 'flow', 225 | id: 'sse-test', 226 | retry: undefined, 227 | })], events); 228 | }); 229 | 230 | it('readAsSSE with dara divided', async function () { 231 | const res = await httpx.request("http://127.0.0.1:8384/sse_with_data_divided", { readTimeout: 5000 }); 232 | assert.strictEqual(res.statusCode, 200); 233 | const events: SSEEvent[] = []; 234 | 235 | for await (const event of $dara.Stream.readAsSSE(res)) { 236 | 237 | events.push(event); 238 | } 239 | assert.strictEqual(events.length, 4); 240 | 241 | assert.deepStrictEqual([new SSEEvent({ 242 | data: '{"count":0}', 243 | event: 'flow', 244 | id: 'sse-test', 245 | retry: 3 246 | }), new SSEEvent({ 247 | data: '{"count":2,"tag":"divided"}', 248 | event: 'flow', 249 | id: 'sse-test', 250 | retry: 3, 251 | }), new SSEEvent({ 252 | data: '{"count":3}', 253 | event: 'flow', 254 | id: 'sse-test', 255 | retry: 3, 256 | }), new SSEEvent({ 257 | data: '{"count":4}', 258 | event: 'flow', 259 | id: 'sse-test', 260 | retry: 3, 261 | })], events); 262 | }); 263 | }); -------------------------------------------------------------------------------- /test/xml.spec.ts: -------------------------------------------------------------------------------- 1 | import * as $dara from '../src/index'; 2 | import 'mocha'; 3 | import assert from 'assert'; 4 | import moment from 'moment'; 5 | 6 | describe('$dara xml', function () { 7 | 8 | const testXml = '\n' + 9 | '\n' + 10 | ' \n' + 11 | ' 1325847523475998\n' + 12 | ' 1325847523475998\n' + 13 | ' \n' + 14 | ' \n' + 15 | ' public-read\n' + 16 | ' \n' + 17 | ''; 18 | const errorXml = '\ 19 | AccessForbidden\ 20 | CORSResponse: CORS is not enabled for this bucket.\ 21 | 5DECB1F6F3150D373335D8D2\ 22 | sdk-oss-test.oss-cn-hangzhou.aliyuncs.com\ 23 | '; 24 | 25 | it('parseXml should ok', async function () { 26 | class GetBucketAclResponseAccessControlPolicyAccessControlList extends $dara.Model { 27 | grant: string; 28 | static names(): { [key: string]: string } { 29 | return { 30 | grant: 'Grant', 31 | }; 32 | } 33 | 34 | static types(): { [key: string]: any } { 35 | return { 36 | grant: 'string', 37 | }; 38 | } 39 | 40 | constructor(map: { [key: string]: any }) { 41 | super(map); 42 | } 43 | 44 | } 45 | 46 | class GetBucketAclResponseAccessControlPolicyOwner extends $dara.Model { 47 | iD: string; 48 | displayName: string; 49 | static names(): { [key: string]: string } { 50 | return { 51 | iD: 'ID', 52 | displayName: 'DisplayName', 53 | }; 54 | } 55 | 56 | static types(): { [key: string]: any } { 57 | return { 58 | iD: 'string', 59 | displayName: 'string', 60 | }; 61 | } 62 | 63 | constructor(map: { [key: string]: any }) { 64 | super(map); 65 | } 66 | 67 | } 68 | 69 | class GetBucketAclResponseAccessControlPolicy extends $dara.Model { 70 | owner: GetBucketAclResponseAccessControlPolicyOwner; 71 | accessControlList: GetBucketAclResponseAccessControlPolicyAccessControlList; 72 | static names(): { [key: string]: string } { 73 | return { 74 | owner: 'Owner', 75 | accessControlList: 'AccessControlList', 76 | }; 77 | } 78 | 79 | static types(): { [key: string]: any } { 80 | return { 81 | owner: GetBucketAclResponseAccessControlPolicyOwner, 82 | accessControlList: GetBucketAclResponseAccessControlPolicyAccessControlList, 83 | }; 84 | } 85 | 86 | constructor(map: { [key: string]: any }) { 87 | super(map); 88 | } 89 | 90 | } 91 | 92 | class GetBucketAclResponse extends $dara.Model { 93 | accessControlPolicy: GetBucketAclResponseAccessControlPolicy; 94 | static names(): { [key: string]: string } { 95 | return { 96 | accessControlPolicy: 'root', 97 | }; 98 | } 99 | 100 | static types(): { [key: string]: any } { 101 | return { 102 | accessControlPolicy: GetBucketAclResponseAccessControlPolicy, 103 | }; 104 | } 105 | 106 | constructor(map: { [key: string]: any }) { 107 | super(map); 108 | } 109 | } 110 | 111 | const data = { 112 | root: { 113 | Owner: { ID: '1325847523475998', DisplayName: '1325847523475998' }, 114 | AccessControlList: { Grant: 'public-read' }, 115 | }, 116 | }; 117 | assert.deepStrictEqual($dara.XML.parseXml(testXml, GetBucketAclResponse), data); 118 | assert.ok($dara.XML.parseXml(errorXml, GetBucketAclResponse)); 119 | try { 120 | $dara.XML.parseXml('ddsfadf', GetBucketAclResponse) 121 | } catch (err) { 122 | assert.ok(err); 123 | return; 124 | } 125 | assert.ok(false); 126 | }); 127 | 128 | it('parseXml with null should ok', async function () { 129 | 130 | const nullXml = '\n' + 131 | ' \n' + 132 | ' oss-example\n' + 133 | ' \n' + 134 | ' \n' + 135 | ' 100\n' + 136 | ' \n' + 137 | ' false\n' + 138 | ' \n' + 139 | ' fun/movie/001.avi\n' + 140 | ' 2012-02-24T08:43:07.000Z\n' + 141 | ' 5B3C1A2E053D763E1B002CC607C5A0FE1****\n' + 142 | ' Normal\n' + 143 | ' 344606\n' + 144 | ' Standard\n' + 145 | ' \n' + 146 | ' 0022012\n' + 147 | ' user-example\n' + 148 | ' \n' + 149 | ' \n' + 150 | ' \n' + 151 | ' fun/movie/007.avi\n' + 152 | ' 2012-02-24T08:43:07.000Z\n' + 153 | ' 5B3C1A2E053D763E1B002CC607C5A0FE2****\n' + 154 | ' Normal\n' + 155 | ' 144606\n' + 156 | ' IA\n' + 157 | ' \n' + 158 | ' 0022012\n' + 159 | ' user-example\n' + 160 | ' \n' + 161 | ' \n' + 162 | ' \n' + 163 | ' oss.jpg\n' + 164 | ' 2012-02-24T08:43:07.000Z\n' + 165 | ' 5B3C1A2E053D763E1B002CC607C5A0FE2****\n' + 166 | ' Normal\n' + 167 | ' 144606\n' + 168 | ' IA\n' + 169 | ' \n' + 170 | ' 0022012\n' + 171 | ' user-example\n' + 172 | ' \n' + 173 | ' \n' + 174 | ' ' 175 | 176 | const data = { 177 | ListBucketResult: { 178 | $: { 179 | xmlns: "http://doc.oss-cn-hangzhou.aliyuncs.com" 180 | }, 181 | Name: "oss-example", 182 | Prefix: "", 183 | Marker: "", 184 | MaxKeys: "100", 185 | Delimiter: "", 186 | IsTruncated: "false", 187 | Contents: [ 188 | { 189 | Key: "fun/movie/001.avi", 190 | LastModified: "2012-02-24T08:43:07.000Z", 191 | ETag: "5B3C1A2E053D763E1B002CC607C5A0FE1****", 192 | Type: "Normal", 193 | Size: "344606", 194 | StorageClass: "Standard", 195 | Owner: { 196 | ID: "0022012", 197 | DisplayName: "user-example" 198 | } 199 | }, 200 | { 201 | Key: "fun/movie/007.avi", 202 | LastModified: "2012-02-24T08:43:07.000Z", 203 | ETag: "5B3C1A2E053D763E1B002CC607C5A0FE2****", 204 | Type: "Normal", 205 | Size: "144606", 206 | StorageClass: "IA", 207 | Owner: { 208 | ID: "0022012", 209 | DisplayName: "user-example" 210 | } 211 | }, 212 | { 213 | Key: "oss.jpg", 214 | LastModified: "2012-02-24T08:43:07.000Z", 215 | ETag: "5B3C1A2E053D763E1B002CC607C5A0FE2****", 216 | Type: "Normal", 217 | Size: "144606", 218 | StorageClass: "IA", 219 | Owner: { 220 | ID: "0022012", 221 | DisplayName: "user-example" 222 | } 223 | } 224 | ] 225 | } 226 | }; 227 | assert.deepStrictEqual($dara.XML.parseXml(nullXml, null), data); 228 | assert.ok($dara.XML.parseXml(errorXml, null)); 229 | 230 | try { 231 | $dara.XML.parseXml('ddsfadf', null) 232 | } catch (err) { 233 | assert.ok(err); 234 | return; 235 | } 236 | assert.ok(false); 237 | }); 238 | it('_toXML should ok', function () { 239 | const data = { 240 | root: { 241 | Owner: { ID: '1325847523475998', DisplayName: '1325847523475998' }, 242 | AccessControlList: { Grant: 'public-read' }, 243 | }, 244 | }; 245 | assert.strictEqual($dara.XML.toXML(data), testXml); 246 | }); 247 | 248 | it('_xmlCast should ok', async function () { 249 | const data: { [key: string]: any } = { 250 | boolean: false, 251 | boolStr: 'true', 252 | number: 1, 253 | NaNNumber: null, 254 | NaN: undefined, 255 | string: 'string', 256 | array: ['string1', 'string2'], 257 | notArray: 'string', 258 | emptyArray: undefined, 259 | classArray: [{ 260 | string: 'string', 261 | }, { 262 | string: 'string' 263 | }], 264 | classMap: '', 265 | map: { 266 | string: 'string', 267 | } 268 | }; 269 | 270 | class TestSubModel extends $dara.Model { 271 | string: string; 272 | static names(): { [key: string]: string } { 273 | return { 274 | string: 'string', 275 | }; 276 | } 277 | 278 | static types(): { [key: string]: any } { 279 | return { 280 | string: 'string', 281 | }; 282 | } 283 | 284 | constructor(map: { [key: string]: any }) { 285 | super(map); 286 | } 287 | } 288 | 289 | class TestModel extends $dara.Model { 290 | boolean: boolean; 291 | boolStr: boolean; 292 | string: string; 293 | number: number; 294 | NaNNumber: number; 295 | array: string[]; 296 | emptyArray: string[]; 297 | notArray: string[]; 298 | map: { [key: string]: any }; 299 | classArray: TestSubModel[]; 300 | classMap: TestSubModel; 301 | static names(): { [key: string]: string } { 302 | return { 303 | boolean: 'boolean', 304 | boolStr: 'boolStr', 305 | string: 'string', 306 | number: 'number', 307 | NaNNumber: 'NaNNumber', 308 | array: 'array', 309 | emptyArray: 'emptyArray', 310 | notArray: 'notArray', 311 | map: 'map', 312 | classArray: 'classArray', 313 | classMap: 'classMap', 314 | }; 315 | } 316 | 317 | static types(): { [key: string]: any } { 318 | return { 319 | boolean: 'boolean', 320 | boolStr: 'boolean', 321 | string: 'string', 322 | number: 'number', 323 | NaNNumber: 'number', 324 | array: { type: 'array', itemType: 'string' }, 325 | emptyArray: { type: 'array', itemType: 'string' }, 326 | notArray: { type: 'array', itemType: 'string' }, 327 | map: 'map', 328 | classArray: { type: 'array', itemType: TestSubModel }, 329 | classMap: TestSubModel, 330 | }; 331 | } 332 | 333 | constructor(map: { [key: string]: any }) { 334 | super(map); 335 | } 336 | } 337 | 338 | assert.deepStrictEqual($dara.XML._xmlCast(data, TestModel), { 339 | "boolean": false, 340 | "boolStr": true, 341 | "number": 1, 342 | "NaNNumber": NaN, 343 | "string": 'string', 344 | "array": ['string1', 'string2'], 345 | "classArray": [{ 346 | "string": 'string', 347 | }, { 348 | "string": 'string' 349 | }], 350 | "notArray": ['string'], 351 | "emptyArray": [], 352 | "classMap": { 353 | "string": '' 354 | }, 355 | "map": { 356 | "string": 'string', 357 | } 358 | }); 359 | }); 360 | }); -------------------------------------------------------------------------------- /test/retry.spec.ts: -------------------------------------------------------------------------------- 1 | import * as $dara from '../src/index'; 2 | import 'mocha'; 3 | import assert from 'assert'; 4 | import moment from 'moment'; 5 | 6 | describe('$dara retry', function () { 7 | class AErr extends $dara.BaseError { 8 | 9 | constructor(map: { [key: string]: any }) { 10 | super(map); 11 | this.name = 'AErr'; 12 | } 13 | } 14 | 15 | class BErr extends $dara.BaseError { 16 | 17 | constructor(map: { [key: string]: any }) { 18 | super(map); 19 | this.name = 'BErr'; 20 | } 21 | } 22 | 23 | class CErr extends $dara.ResponseError { 24 | 25 | constructor(map: { [key: string]: any }) { 26 | super(map); 27 | this.name = 'BErr'; 28 | } 29 | } 30 | 31 | it('init base backoff policy should not okay', function() { 32 | try { 33 | const err = new $dara.BackoffPolicy({}); 34 | const context = new $dara.RetryPolicyContext({ 35 | retriesAttempted: 3, 36 | exception: new AErr({ 37 | code: 'A1Err', 38 | message: 'a1 error', 39 | }) 40 | }); 41 | err.getDelayTime(context); 42 | } catch(err) { 43 | assert.deepStrictEqual(err.message, 'un-implement') 44 | } 45 | }); 46 | 47 | it('shouldRetry should ok', function () { 48 | let context = new $dara.RetryPolicyContext({ 49 | retriesAttempted: 3, 50 | }); 51 | assert.deepStrictEqual($dara.shouldRetry(undefined, context), false); 52 | assert.deepStrictEqual($dara.shouldRetry(null, context), false); 53 | 54 | context = new $dara.RetryPolicyContext({ 55 | retriesAttempted: 0, 56 | }); 57 | assert.deepStrictEqual($dara.shouldRetry(undefined, context), true); 58 | 59 | const condition1 = new $dara.RetryCondition({ 60 | maxAttempts: 3, 61 | exception: ['AErr'], 62 | errorCode: ['A1Err'] 63 | }); 64 | let option = new $dara.RetryOptions({ 65 | retryable: true, 66 | retryCondition: [condition1] 67 | }); 68 | 69 | context = new $dara.RetryPolicyContext({ 70 | retriesAttempted: 3, 71 | exception: new AErr({ 72 | code: 'A1Err', 73 | message: 'a1 error', 74 | }) 75 | }); 76 | assert.deepStrictEqual($dara.shouldRetry(option, context), false); 77 | 78 | option = new $dara.RetryOptions({ 79 | retryable: true, 80 | }); 81 | assert.deepStrictEqual($dara.shouldRetry(option, context), false); 82 | 83 | option = new $dara.RetryOptions({ 84 | retryable: true, 85 | retryCondition: [condition1] 86 | }); 87 | context = new $dara.RetryPolicyContext({ 88 | retriesAttempted: 2, 89 | exception: new AErr({ 90 | code: 'A1Err', 91 | message: 'a1 error', 92 | }) 93 | }); 94 | assert.deepStrictEqual($dara.shouldRetry(option, context), true); 95 | context = new $dara.RetryPolicyContext({ 96 | retriesAttempted: 2, 97 | exception: new AErr({ 98 | code: 'B1Err', 99 | message: 'b1 error', 100 | }) 101 | }); 102 | assert.deepStrictEqual($dara.shouldRetry(option, context), true); 103 | context = new $dara.RetryPolicyContext({ 104 | retriesAttempted: 2, 105 | exception: new BErr({ 106 | code: 'B1Err', 107 | message: 'b1 error', 108 | }) 109 | }); 110 | assert.deepStrictEqual($dara.shouldRetry(option, context), false); 111 | context = new $dara.RetryPolicyContext({ 112 | retriesAttempted: 2, 113 | exception: new BErr({ 114 | code: 'A1Err', 115 | message: 'b1 error', 116 | }) 117 | }); 118 | assert.deepStrictEqual($dara.shouldRetry(option, context), true); 119 | const condition2 = new $dara.RetryCondition({ 120 | maxAttempts: 3, 121 | exception: ['BErr'], 122 | errorCode: ['B1Err'] 123 | }); 124 | option = new $dara.RetryOptions({ 125 | retryable: true, 126 | retryCondition: [condition2], 127 | noRetryCondition: [condition2] 128 | }); 129 | context = new $dara.RetryPolicyContext({ 130 | retriesAttempted: 2, 131 | exception: new AErr({ 132 | code: 'B1Err', 133 | message: 'b1 error', 134 | }) 135 | }); 136 | assert.deepStrictEqual($dara.shouldRetry(option, context), false); 137 | 138 | context = new $dara.RetryPolicyContext({ 139 | retriesAttempted: 2, 140 | exception: new BErr({ 141 | code: 'A1Err', 142 | message: 'a1 error', 143 | }) 144 | }); 145 | assert.deepStrictEqual($dara.shouldRetry(option, context), false); 146 | context = new $dara.RetryPolicyContext({ 147 | retriesAttempted: 1, 148 | exception: new BErr({ 149 | code: 'A1Err', 150 | message: 'b1 error', 151 | }) 152 | }); 153 | assert.deepStrictEqual($dara.shouldRetry(option, context), false); 154 | }); 155 | 156 | it('getBackoffDelay should ok', async function () { 157 | const condition = new $dara.RetryCondition({ 158 | maxAttempts: 3, 159 | exception: ['AErr'], 160 | errorCode: ['A1Err'], 161 | }); 162 | let option = new $dara.RetryOptions({ 163 | retryable: true, 164 | retryCondition: [condition] 165 | }); 166 | 167 | let context = new $dara.RetryPolicyContext({ 168 | retriesAttempted: 2, 169 | exception: new AErr({ 170 | code: 'A1Err', 171 | message: 'a1 error', 172 | }) 173 | }); 174 | 175 | assert.deepStrictEqual($dara.getBackoffDelay(option, context), 100); 176 | 177 | context = new $dara.RetryPolicyContext({ 178 | retriesAttempted: 2, 179 | exception: new BErr({ 180 | code: 'B1Err', 181 | message: 'a1 error', 182 | }) 183 | }); 184 | 185 | assert.deepStrictEqual($dara.getBackoffDelay(option, context), 100); 186 | 187 | const fixedPolicy = $dara.BackoffPolicy.newBackoffPolicy({ 188 | policy: 'Fixed', 189 | period: 1000, 190 | }); 191 | const condition1 = new $dara.RetryCondition({ 192 | maxAttempts: 3, 193 | exception: ['AErr'], 194 | errorCode: ['A1Err'], 195 | backoff: fixedPolicy, 196 | }); 197 | option = new $dara.RetryOptions({ 198 | retryable: true, 199 | retryCondition: [condition1] 200 | }); 201 | context = new $dara.RetryPolicyContext({ 202 | retriesAttempted: 2, 203 | exception: new AErr({ 204 | code: 'A1Err', 205 | message: 'a1 error', 206 | }) 207 | }); 208 | 209 | 210 | assert.deepStrictEqual($dara.getBackoffDelay(option, context), 1000); 211 | 212 | const randomPolicy = $dara.BackoffPolicy.newBackoffPolicy({ 213 | policy: 'Random', 214 | period: 1000, 215 | cap: 10000, 216 | }); 217 | 218 | const condition2 = new $dara.RetryCondition({ 219 | maxAttempts: 3, 220 | exception: ['AErr'], 221 | errorCode: ['A1Err'], 222 | backoff: randomPolicy, 223 | }); 224 | option = new $dara.RetryOptions({ 225 | retryable: true, 226 | retryCondition: [condition2] 227 | }); 228 | 229 | assert.ok($dara.getBackoffDelay(option, context) < 10000); 230 | 231 | const randomPolicy2 = $dara.BackoffPolicy.newBackoffPolicy({ 232 | policy: 'Random', 233 | period: 10000, 234 | cap: 10, 235 | }); 236 | 237 | const condition2Other = new $dara.RetryCondition({ 238 | maxAttempts: 3, 239 | exception: ['AErr'], 240 | errorCode: ['A1Err'], 241 | backoff: randomPolicy2, 242 | }); 243 | option = new $dara.RetryOptions({ 244 | retryable: true, 245 | retryCondition: [condition2Other] 246 | }); 247 | 248 | assert.deepStrictEqual($dara.getBackoffDelay(option, context), 10); 249 | 250 | 251 | let exponentialPolicy = $dara.BackoffPolicy.newBackoffPolicy({ 252 | policy: 'Exponential', 253 | period: 5, 254 | cap: 10000, 255 | }); 256 | 257 | const condition3 = new $dara.RetryCondition({ 258 | maxAttempts: 3, 259 | exception: ['AErr'], 260 | errorCode: ['A1Err'], 261 | backoff: exponentialPolicy, 262 | }); 263 | option = new $dara.RetryOptions({ 264 | retryable: true, 265 | retryCondition: [condition3] 266 | }); 267 | 268 | assert.deepStrictEqual($dara.getBackoffDelay(option, context), 1024); 269 | 270 | exponentialPolicy = $dara.BackoffPolicy.newBackoffPolicy({ 271 | policy: 'Exponential', 272 | period: 10, 273 | cap: 10000, 274 | }); 275 | 276 | const condition4 = new $dara.RetryCondition({ 277 | maxAttempts: 3, 278 | exception: ['AErr'], 279 | errorCode: ['A1Err'], 280 | backoff: exponentialPolicy, 281 | }); 282 | option = new $dara.RetryOptions({ 283 | retryable: true, 284 | retryCondition: [condition4] 285 | }); 286 | 287 | assert.deepStrictEqual($dara.getBackoffDelay(option, context), 10000); 288 | 289 | let equalJitterPolicy = $dara.BackoffPolicy.newBackoffPolicy({ 290 | policy: 'EqualJitter', 291 | period: 5, 292 | cap: 10000, 293 | }); 294 | 295 | const condition5 = new $dara.RetryCondition({ 296 | maxAttempts: 2, 297 | exception: ['AErr'], 298 | errorCode: ['A1Err'], 299 | backoff: equalJitterPolicy, 300 | }); 301 | option = new $dara.RetryOptions({ 302 | retryable: true, 303 | retryCondition: [condition5] 304 | }); 305 | let backoffTime = $dara.getBackoffDelay(option, context) 306 | assert.ok(backoffTime > 512 && backoffTime < 1024); 307 | 308 | equalJitterPolicy = $dara.BackoffPolicy.newBackoffPolicy({ 309 | policy: 'EqualJitter', 310 | period: 10, 311 | cap: 10000, 312 | }); 313 | 314 | const condition6 = new $dara.RetryCondition({ 315 | maxAttempts: 3, 316 | exception: ['AErr'], 317 | errorCode: ['A1Err'], 318 | backoff: equalJitterPolicy, 319 | }); 320 | option = new $dara.RetryOptions({ 321 | retryable: true, 322 | retryCondition: [condition6] 323 | }); 324 | backoffTime = $dara.getBackoffDelay(option, context) 325 | assert.ok(backoffTime > 5000 && backoffTime < 10000); 326 | 327 | 328 | let fullJitterPolicy = $dara.BackoffPolicy.newBackoffPolicy({ 329 | policy: 'fullJitter', 330 | period: 5, 331 | cap: 10000, 332 | }); 333 | 334 | const condition7 = new $dara.RetryCondition({ 335 | maxAttempts: 2, 336 | exception: ['AErr'], 337 | errorCode: ['A1Err'], 338 | backoff: fullJitterPolicy, 339 | }); 340 | option = new $dara.RetryOptions({ 341 | retryable: true, 342 | retryCondition: [condition7] 343 | }); 344 | backoffTime = $dara.getBackoffDelay(option, context) 345 | assert.ok(backoffTime >= 0 && backoffTime < 1024); 346 | 347 | fullJitterPolicy = $dara.BackoffPolicy.newBackoffPolicy({ 348 | policy: 'ExponentialWithFullJitter', 349 | period: 10, 350 | cap: 10000, 351 | }); 352 | 353 | const condition8 = new $dara.RetryCondition({ 354 | maxAttempts: 3, 355 | exception: ['AErr'], 356 | errorCode: ['A1Err'], 357 | backoff: fullJitterPolicy, 358 | }); 359 | option = new $dara.RetryOptions({ 360 | retryable: true, 361 | retryCondition: [condition8] 362 | }); 363 | backoffTime = $dara.getBackoffDelay(option, context) 364 | assert.ok(backoffTime >= 0 && backoffTime < 10000); 365 | 366 | const condition9 = new $dara.RetryCondition({ 367 | maxAttempts: 3, 368 | exception: ['AErr'], 369 | errorCode: ['A1Err'], 370 | backoff: fullJitterPolicy, 371 | maxDelay: 1000, 372 | }); 373 | option = new $dara.RetryOptions({ 374 | retryable: true, 375 | retryCondition: [condition9] 376 | }); 377 | backoffTime = $dara.getBackoffDelay(option, context) 378 | assert.ok(backoffTime >= 0 && backoffTime <= 1000); 379 | 380 | 381 | fullJitterPolicy = $dara.BackoffPolicy.newBackoffPolicy({ 382 | policy: 'ExponentialWithFullJitter', 383 | period: 100, 384 | cap: 10000 * 10000, 385 | }); 386 | 387 | const condition12 = new $dara.RetryCondition({ 388 | maxAttempts: 2, 389 | exception: ['AErr'], 390 | errorCode: ['A1Err'], 391 | backoff: fullJitterPolicy, 392 | }); 393 | option = new $dara.RetryOptions({ 394 | retryable: true, 395 | retryCondition: [condition12] 396 | }); 397 | backoffTime = $dara.getBackoffDelay(option, context); 398 | assert.ok(backoffTime >= 0 && backoffTime <= 120 * 1000); 399 | 400 | context = new $dara.RetryPolicyContext({ 401 | retriesAttempted: 2, 402 | exception: new CErr({ 403 | code: 'CErr', 404 | message: 'c error', 405 | retryAfter: 3000 406 | }) 407 | }); 408 | const condition10 = new $dara.RetryCondition({ 409 | maxAttempts: 3, 410 | exception: ['CErr'], 411 | errorCode: ['CErr'], 412 | backoff: fullJitterPolicy, 413 | maxDelay: 5000, 414 | }); 415 | option = new $dara.RetryOptions({ 416 | retryable: true, 417 | retryCondition: [condition10] 418 | }); 419 | backoffTime = $dara.getBackoffDelay(option, context) 420 | assert.strictEqual(backoffTime, 3000); 421 | 422 | const condition11 = new $dara.RetryCondition({ 423 | maxAttempts: 3, 424 | exception: ['CErr'], 425 | errorCode: ['CErr'], 426 | backoff: fullJitterPolicy, 427 | maxDelay: 1000, 428 | }); 429 | option = new $dara.RetryOptions({ 430 | retryable: true, 431 | retryCondition: [condition11] 432 | }); 433 | backoffTime = $dara.getBackoffDelay(option, context) 434 | assert.strictEqual(backoffTime, 1000); 435 | 436 | 437 | 438 | }); 439 | 440 | 441 | 442 | }); -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 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, Writable } from 'stream'; 5 | import * as httpx from 'httpx'; 6 | import { parse } from 'url'; 7 | import { RetryOptions } from './retry'; 8 | import { BaseError } from './error'; 9 | import * as $tea from '@alicloud/tea-typescript'; 10 | 11 | type TeaDict = { [key: string]: string }; 12 | type TeaObject = { [key: string]: any }; 13 | type AgentOptions = { keepAlive: boolean }; 14 | 15 | export class BytesReadable extends Readable { 16 | value: Buffer 17 | 18 | constructor(value: string | Buffer) { 19 | super(); 20 | if (typeof value === 'string') { 21 | this.value = Buffer.from(value); 22 | } else if (Buffer.isBuffer(value)) { 23 | this.value = value; 24 | } 25 | } 26 | 27 | _read() { 28 | this.push(this.value); 29 | this.push(null); 30 | } 31 | } 32 | 33 | export class Request { 34 | protocol: string; 35 | port: number; 36 | method: string; 37 | pathname: string; 38 | query: TeaDict; 39 | headers: TeaDict; 40 | body: Readable; 41 | 42 | constructor() { 43 | this.headers = {}; 44 | this.query = {}; 45 | } 46 | } 47 | 48 | export class Response { 49 | statusCode: number; 50 | statusMessage: string; 51 | headers: TeaDict; 52 | body: IncomingMessage; 53 | constructor(httpResponse: IncomingMessage) { 54 | this.statusCode = httpResponse.statusCode; 55 | this.statusMessage = httpResponse.statusMessage; 56 | this.headers = this.convertHeaders(httpResponse.headers); 57 | this.body = httpResponse; 58 | } 59 | 60 | convertHeaders(headers: IncomingHttpHeaders): TeaDict { 61 | const results: TeaDict = {}; 62 | const keys = Object.keys(headers); 63 | for (let index = 0; index < keys.length; index++) { 64 | const key = keys[index]; 65 | results[key] = headers[key]; 66 | } 67 | return results; 68 | } 69 | 70 | async readBytes(): Promise { 71 | const buff = await httpx.read(this.body, ''); 72 | return buff; 73 | } 74 | } 75 | 76 | function buildURL(request: Request) { 77 | let url = `${request.protocol}://${request.headers['host']}`; 78 | if (request.port) { 79 | url += `:${request.port}`; 80 | } 81 | url += `${request.pathname}`; 82 | const urlInfo = parse(url); 83 | if (request.query && Object.keys(request.query).length > 0) { 84 | if (urlInfo.query) { 85 | url += `&${querystring.stringify(request.query)}`; 86 | } else { 87 | url += `?${querystring.stringify(request.query)}`; 88 | } 89 | } 90 | return url; 91 | } 92 | 93 | function isModelClass(t: any): boolean { 94 | if (!t) { 95 | return false; 96 | } 97 | return typeof t.types === 'function' && typeof t.names === 'function'; 98 | } 99 | 100 | export async function doAction(request: Request, runtime: TeaObject = null): Promise { 101 | const url = buildURL(request); 102 | const method = (request.method || 'GET').toUpperCase(); 103 | const options: httpx.Options = { 104 | method: method, 105 | headers: request.headers 106 | }; 107 | 108 | if (method !== 'GET' && method !== 'HEAD') { 109 | options.data = request.body; 110 | } 111 | 112 | if (runtime) { 113 | if (typeof runtime.timeout !== 'undefined') { 114 | options.timeout = Number(runtime.timeout); 115 | } 116 | 117 | if (typeof runtime.readTimeout !== 'undefined') { 118 | options.readTimeout = Number(runtime.readTimeout); 119 | } 120 | 121 | if (typeof runtime.connectTimeout !== 'undefined') { 122 | options.connectTimeout = Number(runtime.connectTimeout); 123 | } 124 | 125 | if (typeof runtime.ignoreSSL !== 'undefined') { 126 | options.rejectUnauthorized = !runtime.ignoreSSL; 127 | } 128 | 129 | if (typeof runtime.key !== 'undefined') { 130 | options.key = String(runtime.key); 131 | } 132 | 133 | if (typeof runtime.cert !== 'undefined') { 134 | options.cert = String(runtime.cert); 135 | } 136 | 137 | if (typeof runtime.ca !== 'undefined') { 138 | options.ca = String(runtime.ca); 139 | } 140 | 141 | // keepAlive: default true 142 | const agentOptions: AgentOptions = { 143 | keepAlive: true, 144 | }; 145 | if (typeof runtime.keepAlive !== 'undefined') { 146 | agentOptions.keepAlive = runtime.keepAlive; 147 | if (request.protocol && request.protocol.toLowerCase() === 'https') { 148 | options.agent = new HttpsAgent(agentOptions); 149 | } else { 150 | options.agent = new HttpAgent(agentOptions); 151 | } 152 | } 153 | 154 | 155 | } 156 | 157 | const response = await httpx.request(url, options); 158 | 159 | return new Response(response); 160 | } 161 | 162 | 163 | 164 | 165 | 166 | function getValue(type: any, value: any): any { 167 | if (typeof type === 'string') { 168 | // basic type 169 | return value; 170 | } 171 | if (type.type === 'array') { 172 | if (!Array.isArray(value)) { 173 | throw new Error(`expect: array, actual: ${typeof value}`); 174 | } 175 | return value.map((item: any) => { 176 | return getValue(type.itemType, item); 177 | }); 178 | } 179 | if (typeof type === 'function') { 180 | if (isModelClass(type)) { 181 | return new type(value); 182 | } 183 | return value; 184 | } 185 | return value; 186 | } 187 | 188 | export function toMap(value: any = undefined, withoutStream: boolean = false): any { 189 | if (typeof value === 'undefined' || value == null) { 190 | return null; 191 | } 192 | 193 | if (value instanceof Model) { 194 | return value.toMap(withoutStream); 195 | } 196 | 197 | // 如果是另一个版本的 tea-typescript 创建的 model,instanceof 会判断不通过 198 | // 这里做一下处理 199 | if (typeof value.toMap === 'function') { 200 | return value.toMap(withoutStream); 201 | } 202 | 203 | if (Array.isArray(value)) { 204 | return value.map((item) => { 205 | return toMap(item, withoutStream); 206 | }) 207 | } 208 | 209 | if(withoutStream && (value instanceof Readable || value instanceof Writable)) { 210 | return null; 211 | } 212 | 213 | return value; 214 | } 215 | 216 | export class Model extends $tea.Model { 217 | [key: string]: any 218 | 219 | constructor(map?: TeaObject) { 220 | super(); 221 | if (map == null) { 222 | return; 223 | } 224 | 225 | const clz = this.constructor; 226 | const names = clz.names(); 227 | const types = clz.types(); 228 | Object.keys(names).forEach((name => { 229 | const value = map[name]; 230 | if (value === undefined || value === null) { 231 | return; 232 | } 233 | const type = types[name]; 234 | this[name] = getValue(type, value); 235 | })); 236 | } 237 | 238 | validate(): void {} 239 | 240 | copyWithoutStream(): T { 241 | const map: TeaObject = this.toMap(true); 242 | const clz = this.constructor; 243 | return new clz(map); 244 | } 245 | 246 | toMap(withoutStream: boolean = false): TeaObject { 247 | const map: TeaObject = {}; 248 | const clz = this.constructor; 249 | const names = clz.names(); 250 | Object.keys(names).forEach((name => { 251 | const originName = names[name]; 252 | const value = this[name]; 253 | if (typeof value === 'undefined' || value == null) { 254 | return; 255 | } 256 | map[originName] = toMap(value, withoutStream); 257 | })); 258 | return map; 259 | } 260 | 261 | static validateRequired(key: string, value: any) { 262 | if(value === null || typeof value === 'undefined') { 263 | throw new BaseError({ 264 | code: 'SDK.ValidateError', 265 | message: `${key} is required.`, 266 | }); 267 | } 268 | } 269 | 270 | static validateMaxLength(key: string, value: any, max: number) { 271 | if(value === null || typeof value === 'undefined') { 272 | return; 273 | } 274 | if(value.length > max) { 275 | throw new BaseError({ 276 | code: 'SDK.ValidateError', 277 | message: `${key} is exceed max-length: ${max}.`, 278 | }); 279 | } 280 | } 281 | 282 | static validateMinLength(key: string, value: any, min: number) { 283 | if(value === null || typeof value === 'undefined') { 284 | return; 285 | } 286 | if(value.length < min) { 287 | throw new BaseError({ 288 | code: 'SDK.ValidateError', 289 | message: `${key} is exceed min-length: ${min}.`, 290 | }); 291 | } 292 | } 293 | 294 | static validateMaximum(key: string, value: number | undefined, max: number) { 295 | if(value === null || typeof value === 'undefined') { 296 | return; 297 | } 298 | if(value > max) { 299 | throw new BaseError({ 300 | code: 'SDK.ValidateError', 301 | message: `${key} cannot be greater than ${max}.`, 302 | }); 303 | } 304 | } 305 | 306 | static validateMinimum(key: string, value: number | undefined, min: number) { 307 | if(value === null || typeof value === 'undefined') { 308 | return; 309 | } 310 | if(value < min) { 311 | throw new BaseError({ 312 | code: 'SDK.ValidateError', 313 | message: `${key} cannot be less than ${min}.`, 314 | }); 315 | } 316 | } 317 | 318 | static validatePattern(key: string, value: any, val: string) { 319 | if(value === null || typeof value === 'undefined') { 320 | return; 321 | } 322 | const reg = new RegExp(val); 323 | if(!reg.test(value)) { 324 | throw new BaseError({ 325 | code: 'SDK.ValidateError', 326 | message: `${key} is not match ${val}.`, 327 | }); 328 | } 329 | } 330 | 331 | static validateArray(data?: any[]) { 332 | if(data === null || typeof data === 'undefined') { 333 | return; 334 | } 335 | data.map(ele => { 336 | if(!ele) { 337 | return; 338 | } 339 | if(ele instanceof Model || typeof ele.validate === 'function') { 340 | ele.validate(); 341 | } else if(Array.isArray(ele)) { 342 | Model.validateArray(ele); 343 | } else if(ele instanceof Object) { 344 | Model.validateMap(ele); 345 | } 346 | }) 347 | } 348 | 349 | static validateMap(data?: { [key: string]: any }) { 350 | if(data === null || typeof data === 'undefined') { 351 | return; 352 | } 353 | Object.keys(data).map(key => { 354 | const ele = data[key]; 355 | if(!ele) { 356 | return; 357 | } 358 | if(ele instanceof Model || typeof ele.validate === 'function') { 359 | ele.validate(); 360 | } else if(Array.isArray(ele)) { 361 | Model.validateArray(ele); 362 | } else if(ele instanceof Object) { 363 | Model.validateMap(ele); 364 | } 365 | }) 366 | } 367 | } 368 | 369 | 370 | export class FileField extends Model { 371 | filename: string; 372 | contentType: string; 373 | content: Readable; 374 | static names(): { [key: string]: string } { 375 | return { 376 | filename: 'filename', 377 | contentType: 'contentType', 378 | content: 'content', 379 | }; 380 | } 381 | 382 | static types(): { [key: string]: any } { 383 | return { 384 | filename: 'string', 385 | contentType: 'string', 386 | content: 'Readable', 387 | }; 388 | } 389 | 390 | constructor(map?: { [key: string]: any }) { 391 | super(map); 392 | } 393 | } 394 | 395 | export class ExtendsParameters extends $tea.Model { 396 | headers?: { [key: string]: string }; 397 | queries?: { [key: string]: string }; 398 | static names(): { [key: string]: string } { 399 | return { 400 | headers: 'headers', 401 | queries: 'queries', 402 | }; 403 | } 404 | 405 | static types(): { [key: string]: any } { 406 | return { 407 | headers: { 'type': 'map', 'keyType': 'string', 'valueType': 'string' }, 408 | queries: { 'type': 'map', 'keyType': 'string', 'valueType': 'string' }, 409 | }; 410 | } 411 | 412 | constructor(map?: { [key: string]: any }) { 413 | super(map); 414 | } 415 | } 416 | 417 | export class RuntimeOptions extends $tea.Model { 418 | retryOptions?: RetryOptions; 419 | autoretry?: boolean; 420 | ignoreSSL?: boolean; 421 | key?: string; 422 | cert?: string; 423 | ca?: string; 424 | maxAttempts?: number; 425 | backoffPolicy?: string; 426 | backoffPeriod?: number; 427 | readTimeout?: number; 428 | connectTimeout?: number; 429 | httpProxy?: string; 430 | httpsProxy?: string; 431 | noProxy?: string; 432 | maxIdleConns?: number; 433 | keepAlive?: boolean; 434 | extendsParameters?: ExtendsParameters; 435 | static names(): { [key: string]: string } { 436 | return { 437 | autoretry: 'autoretry', 438 | ignoreSSL: 'ignoreSSL', 439 | key: 'key', 440 | cert: 'cert', 441 | ca: 'ca', 442 | maxAttempts: 'max_attempts', 443 | backoffPolicy: 'backoff_policy', 444 | backoffPeriod: 'backoff_period', 445 | readTimeout: 'readTimeout', 446 | connectTimeout: 'connectTimeout', 447 | httpProxy: 'httpProxy', 448 | httpsProxy: 'httpsProxy', 449 | noProxy: 'noProxy', 450 | maxIdleConns: 'maxIdleConns', 451 | keepAlive: 'keepAlive', 452 | extendsParameters: 'extendsParameters', 453 | }; 454 | } 455 | 456 | static types(): { [key: string]: any } { 457 | return { 458 | retryOptions: RetryOptions, 459 | autoretry: 'boolean', 460 | ignoreSSL: 'boolean', 461 | key: 'string', 462 | cert: 'string', 463 | ca: 'string', 464 | maxAttempts: 'number', 465 | backoffPolicy: 'string', 466 | backoffPeriod: 'number', 467 | readTimeout: 'number', 468 | connectTimeout: 'number', 469 | httpProxy: 'string', 470 | httpsProxy: 'string', 471 | noProxy: 'string', 472 | maxIdleConns: 'number', 473 | keepAlive: 'boolean', 474 | extendsParameters: ExtendsParameters, 475 | }; 476 | } 477 | 478 | constructor(map?: { [key: string]: any }) { 479 | super(map); 480 | } 481 | } 482 | 483 | export function cast(obj: any, t: T): T { 484 | if (!obj) { 485 | throw new Error('can not cast to Map'); 486 | } 487 | 488 | if (typeof obj !== 'object') { 489 | throw new Error('can not cast to Map'); 490 | } 491 | 492 | const map = obj as TeaObject; 493 | const clz = t.constructor as any; 494 | const names: TeaDict = clz.names(); 495 | const types: TeaObject = clz.types(); 496 | Object.keys(names).forEach((key) => { 497 | const originName = names[key]; 498 | const value = map[originName]; 499 | const type = types[key]; 500 | if (typeof value === 'undefined' || value == null) { 501 | return; 502 | } 503 | if (typeof type === 'string') { 504 | if (type === 'Readable' || 505 | type === 'Writable' || 506 | type === 'map' || 507 | type === 'Buffer' || 508 | type === 'any' || 509 | typeof value === type) { 510 | (t)[key] = value; 511 | return; 512 | } 513 | if (type === 'string' && 514 | (typeof value === 'number' || 515 | typeof value === 'boolean')) { 516 | (t)[key] = value.toString(); 517 | return; 518 | } 519 | if (type === 'boolean') { 520 | if (value === 1 || value === 0) { 521 | (t)[key] = !!value; 522 | return; 523 | } 524 | if (value === 'true' || value === 'false') { 525 | (t)[key] = value === 'true'; 526 | return; 527 | } 528 | } 529 | 530 | if (type === 'number' && typeof value === 'string') { 531 | if (value.match(/^\d*$/)) { 532 | (t)[key] = parseInt(value); 533 | return; 534 | } 535 | if (value.match(/^[\.\d]*$/)) { 536 | (t)[key] = parseFloat(value); 537 | return; 538 | } 539 | } 540 | throw new Error(`type of ${key} is mismatch, expect ${type}, but ${typeof value}`); 541 | } else if (type.type === 'map') { 542 | if (!(value instanceof Object)) { 543 | throw new Error(`type of ${key} is mismatch, expect object, but ${typeof value}`); 544 | } 545 | (t)[key] = value; 546 | } else if (type.type === 'array') { 547 | if (!Array.isArray(value)) { 548 | throw new Error(`type of ${key} is mismatch, expect array, but ${typeof value}`); 549 | } 550 | if (typeof type.itemType === 'function') { 551 | (t)[key] = value.map((d: any) => { 552 | if (isModelClass(type.itemType)) { 553 | return cast(d, new type.itemType({})); 554 | } 555 | return d; 556 | }); 557 | } else { 558 | (t)[key] = value; 559 | } 560 | 561 | } else if (typeof type === 'function') { 562 | if (!(value instanceof Object)) { 563 | throw new Error(`type of ${key} is mismatch, expect object, but ${typeof value}`); 564 | } 565 | if (isModelClass(type)) { 566 | (t)[key] = cast(value, new type({})); 567 | return; 568 | } 569 | (t)[key] = value; 570 | } else { 571 | 572 | } 573 | }); 574 | 575 | return t; 576 | } 577 | 578 | export function allowRetry(retry: TeaObject, retryTimes: number, startTime: number): boolean { 579 | // 还未重试 580 | if (retryTimes === 0) { 581 | return true; 582 | } 583 | 584 | if (retry.retryable !== true) { 585 | return false; 586 | } 587 | 588 | if (retry.policy === 'never') { 589 | return false; 590 | } 591 | 592 | if (retry.policy === 'always') { 593 | return true; 594 | } 595 | 596 | if (retry.policy === 'simple') { 597 | return (retryTimes < retry['maxAttempts']); 598 | } 599 | 600 | if (retry.policy === 'timeout') { 601 | return Date.now() - startTime < retry.timeout; 602 | } 603 | 604 | if (retry.maxAttempts && typeof retry.maxAttempts === 'number') { 605 | return retry.maxAttempts >= retryTimes; 606 | } 607 | 608 | // 默认不重试 609 | return false; 610 | } 611 | 612 | export function getBackoffTime(backoff: TeaObject, retryTimes: number): number { 613 | if (retryTimes === 0) { 614 | // 首次调用,不使用退避策略 615 | return 0; 616 | } 617 | 618 | if (backoff.policy === 'no') { 619 | // 不退避 620 | return 0; 621 | } 622 | 623 | if (backoff.policy === 'fixed') { 624 | // 固定退避 625 | return backoff.period; 626 | } 627 | 628 | if (backoff.policy === 'random') { 629 | // 随机退避 630 | const min = backoff['minPeriod']; 631 | const max = backoff['maxPeriod']; 632 | return min + (max - min) * Math.random(); 633 | } 634 | 635 | if (backoff.policy === 'exponential') { 636 | // 指数退避 637 | const init = backoff.initial; 638 | const multiplier = backoff.multiplier; 639 | const time = init * Math.pow(1 + multiplier, retryTimes - 1); 640 | const max = backoff.max; 641 | return Math.min(time, max); 642 | } 643 | 644 | if (backoff.policy === 'exponential_random') { 645 | // 指数随机退避 646 | const init = backoff.initial; 647 | const multiplier = backoff.multiplier; 648 | const time = init * Math.pow(1 + multiplier, retryTimes - 1); 649 | const max = backoff.max; 650 | return Math.min(time * (0.5 + Math.random()), max); 651 | } 652 | 653 | return 0; 654 | } 655 | 656 | export function isRetryable(err: Error): boolean { 657 | if (typeof err === 'undefined' || err === null) { 658 | return false; 659 | } 660 | return err.name === 'RetryError'; 661 | } 662 | 663 | -------------------------------------------------------------------------------- /test/core.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import http from 'http'; 4 | import url from 'url'; 5 | import 'mocha'; 6 | import assert from 'assert'; 7 | import { AddressInfo } from 'net'; 8 | import { Readable, Writable } from 'stream'; 9 | 10 | import * as $dara from '../src/index'; 11 | 12 | const server = http.createServer((req, res) => { 13 | const urlObj = url.parse(req.url, true); 14 | if (urlObj.pathname === '/timeout') { 15 | setTimeout(() => { 16 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 17 | res.end('Hello world!'); 18 | }, 5000); 19 | } else if (urlObj.pathname === '/keepAlive') { 20 | res.writeHead(200, { 21 | 'Content-Type': 'text/plain', 22 | 'Client-Keep-Alive': req.headers.connection 23 | }); 24 | res.end('Hello world!'); 25 | } else if (urlObj.pathname === '/query') { 26 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 27 | res.end(JSON.stringify(urlObj.query)); 28 | } else { 29 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 30 | res.end('Hello world!'); 31 | } 32 | }); 33 | 34 | function read(readable: Readable): Promise { 35 | return new Promise((resolve, reject) => { 36 | const buffers: Buffer[] = []; 37 | readable.on('data', function (chunk) { 38 | buffers.push(chunk); 39 | }); 40 | readable.on('end', function () { 41 | resolve(Buffer.concat(buffers)); 42 | }); 43 | }); 44 | } 45 | 46 | describe('$dara', function () { 47 | 48 | before((done) => { 49 | server.listen(0, done); 50 | }); 51 | 52 | after(function (done) { 53 | this.timeout(10000); 54 | server.close(done); 55 | }); 56 | 57 | describe('cast', function () { 58 | 59 | it('cast should ok', function () { 60 | class ListInfo { 61 | length: number 62 | constructor(length: number) { 63 | this.length = length; 64 | } 65 | } 66 | class TypeInfo { 67 | type: string 68 | constructor(type: string) { 69 | this.type = type; 70 | } 71 | } 72 | const testStream = new Readable(); 73 | const testWStream = new Writable(); 74 | const meta: { [key: string]: string } = { 75 | 'habits': 'dota' 76 | }; 77 | const listInfo = new ListInfo(2); 78 | const typeList = [new TypeInfo('user'), new TypeInfo('admin')]; 79 | const info = { 80 | info: 'ok' 81 | }; 82 | const data = { 83 | items: [ 84 | { 85 | domain_id: 'sz16', 86 | user_id: 'DING-EthqxxiPlOSS6gxxixxE', 87 | avatar: '', 88 | created_at: 1568773418121, 89 | updated_at: 1568773418121, 90 | email: '', 91 | nick_name: '朴X', 92 | strong: 'true', 93 | phone: '', 94 | role: 'user', 95 | status: 'enabled', 96 | titles: ['Node.js官方认证开发者', '深入浅出Node.js作者'], 97 | user_name: '朴X', 98 | description: '', 99 | default_drive_id: '', 100 | meta, 101 | extra: info, 102 | file: testWStream, 103 | float_id: '3.1415' 104 | }, 105 | { 106 | domain_id: 'sz16', 107 | user_id: 'DING-aexxfgfelxx', 108 | avatar: '', 109 | created_at: 1568732914442, 110 | updated_at: 0, 111 | email: '', 112 | nick_name: '普X', 113 | strong: 1, 114 | phone: '', 115 | role: 'user', 116 | status: 'enabled', 117 | titles: ['写代码的'], 118 | user_name: '普X', 119 | description: '', 120 | default_drive_id: '', 121 | extra: 'simple', 122 | }, 123 | { 124 | domain_id: 1234, 125 | user_id: 'DING-aefgfesd', 126 | avatar: '', 127 | created_at: '1568732914442', 128 | updated_at: '0', 129 | email: '', 130 | nick_name: 'test', 131 | strong: 'false', 132 | phone: '', 133 | role: 'user', 134 | status: 'enabled', 135 | titles: ['测试工程师'], 136 | user_name: 'TS', 137 | description: '', 138 | default_drive_id: '', 139 | extra: 'simple', 140 | } 141 | ], 142 | superadmin: { 143 | domain_id: 'sz16', 144 | user_id: 'superadmin', 145 | avatar: '', 146 | created_at: 1568732914502, 147 | updated_at: 0, 148 | email: '', 149 | nick_name: 'superadmin', 150 | strong: false, 151 | phone: '', 152 | role: 'superadmin', 153 | status: 'enabled', 154 | titles: ['superadmin'], 155 | user_name: 'superadmin', 156 | description: '', 157 | default_drive_id: '', 158 | meta 159 | }, 160 | stream: testStream, 161 | list_info: listInfo, 162 | type_list: typeList, 163 | next_marker: 'next marker' 164 | }; 165 | 166 | class BaseUserResponse extends $dara.Model { 167 | avatar?: string 168 | createdAt?: number 169 | defaultDriveId?: string 170 | description?: string 171 | domainId?: string 172 | email?: string 173 | nickName?: string 174 | strong?: boolean 175 | phone?: string 176 | role?: string 177 | status?: string 178 | titles?: string 179 | updatedAt?: number 180 | userId?: string 181 | userName?: string 182 | meta?: { [key: string]: any } 183 | file: Writable 184 | extra?: any 185 | floatId: number 186 | static names(): { [key: string]: string } { 187 | return { 188 | avatar: 'avatar', 189 | createdAt: 'created_at', 190 | defaultDriveId: 'default_drive_id', 191 | description: 'description', 192 | domainId: 'domain_id', 193 | email: 'email', 194 | nickName: 'nick_name', 195 | strong: 'strong', 196 | phone: 'phone', 197 | role: 'role', 198 | status: 'status', 199 | titles: 'titles', 200 | updatedAt: 'updated_at', 201 | userId: 'user_id', 202 | userName: 'user_name', 203 | meta: 'meta', 204 | extra: 'extra', 205 | file: 'file', 206 | floatId: 'float_id', 207 | }; 208 | } 209 | 210 | static types(): { [key: string]: any } { 211 | return { 212 | avatar: 'string', 213 | createdAt: 'number', 214 | defaultDriveId: 'string', 215 | description: 'string', 216 | domainId: 'string', 217 | email: 'string', 218 | nickName: 'string', 219 | strong: 'boolean', 220 | phone: 'string', 221 | role: 'string', 222 | status: 'string', 223 | titles: { type: 'array', itemType: 'string' }, 224 | updatedAt: 'number', 225 | userId: 'string', 226 | userName: 'string', 227 | meta: { 'type': 'map', 'keyType': 'string', 'valueType': 'any' }, 228 | extra: 'any', 229 | file: 'Writable', 230 | floatId: 'number', 231 | }; 232 | } 233 | 234 | constructor(map: { [key: string]: any }) { 235 | super(map); 236 | } 237 | } 238 | 239 | class ListUserResponse extends $dara.Model { 240 | items?: BaseUserResponse[] 241 | superadmin?: BaseUserResponse 242 | stream?: Readable 243 | nextMarker?: string 244 | listInfo?: ListInfo 245 | typeList?: TypeInfo 246 | static names(): { [key: string]: string } { 247 | return { 248 | items: 'items', 249 | superadmin: 'superadmin', 250 | stream: 'stream', 251 | nextMarker: 'next_marker', 252 | listInfo: 'list_info', 253 | typeList: 'type_list', 254 | }; 255 | } 256 | 257 | static types(): { [key: string]: any } { 258 | return { 259 | items: { 'type': 'array', 'itemType': BaseUserResponse }, 260 | superadmin: BaseUserResponse, 261 | stream: 'Readable', 262 | nextMarker: 'string', 263 | listInfo: ListInfo, 264 | typeList: { 'type': 'array', 'itemType': TypeInfo }, 265 | }; 266 | } 267 | 268 | constructor(map: { [key: string]: any }) { 269 | super(map); 270 | } 271 | } 272 | 273 | const response = $dara.cast(data, new ListUserResponse({})); 274 | assert.deepStrictEqual(response, new ListUserResponse({ 275 | items: [ 276 | new BaseUserResponse({ 277 | 'avatar': '', 278 | 'createdAt': 1568773418121, 279 | 'defaultDriveId': '', 280 | 'description': '', 281 | 'domainId': 'sz16', 282 | 'strong': true, 283 | 'email': '', 284 | 'nickName': '朴X', 285 | 'phone': '', 286 | 'role': 'user', 287 | 'status': 'enabled', 288 | 'titles': [ 'Node.js官方认证开发者', '深入浅出Node.js作者'], 289 | 'updatedAt': 1568773418121, 290 | 'userId': 'DING-EthqxxiPlOSS6gxxixxE', 291 | 'userName': '朴X', 292 | 'meta': meta, 293 | 'extra': { info: 'ok' }, 294 | 'file': testWStream, 295 | 'floatId': 3.1415 296 | }), 297 | new BaseUserResponse({ 298 | 'avatar': '', 299 | 'createdAt': 1568732914442, 300 | 'defaultDriveId': '', 301 | 'description': '', 302 | 'domainId': 'sz16', 303 | 'email': '', 304 | 'nickName': '普X', 305 | 'strong': true, 306 | 'phone': '', 307 | 'role': 'user', 308 | 'status': 'enabled', 309 | 'titles': ['写代码的'], 310 | 'updatedAt': 0, 311 | 'userId': 'DING-aexxfgfelxx', 312 | 'userName': '普X', 313 | 'meta': undefined, 314 | 'extra': 'simple', 315 | 'floatId': undefined 316 | }), 317 | new BaseUserResponse({ 318 | 'avatar': '', 319 | 'createdAt': 1568732914442, 320 | 'defaultDriveId': '', 321 | 'description': '', 322 | 'domainId': '1234', 323 | 'email': '', 324 | 'nickName': 'test', 325 | 'strong': false, 326 | 'phone': '', 327 | 'role': 'user', 328 | 'status': 'enabled', 329 | 'titles': ['测试工程师'], 330 | 'updatedAt': 0, 331 | 'userId': 'DING-aefgfesd', 332 | 'userName': 'TS', 333 | 'meta': undefined, 334 | 'extra': 'simple', 335 | 'floatId': undefined 336 | }) 337 | ], 338 | 'superadmin': new BaseUserResponse({ 339 | 'avatar': '', 340 | 'createdAt': 1568732914502, 341 | 'defaultDriveId': '', 342 | 'description': '', 343 | 'domainId': 'sz16', 344 | 'email': '', 345 | 'nickName': 'superadmin', 346 | 'strong': false, 347 | 'phone': '', 348 | 'role': 'superadmin', 349 | 'status': 'enabled', 350 | 'titles': ['superadmin'], 351 | 'updatedAt': 0, 352 | 'userId': 'superadmin', 353 | 'userName': 'superadmin', 354 | 'meta': meta 355 | }), 356 | 'stream': testStream, 357 | 'listInfo': listInfo, 358 | 'typeList': typeList, 359 | 'nextMarker': 'next marker' 360 | })); 361 | }); 362 | 363 | it('cast wrong type should error', function () { 364 | class MetaInfo { 365 | meta: string 366 | constructor(meta: string) { 367 | this.meta = meta; 368 | } 369 | } 370 | class MapInfo { 371 | map: { [key: string]: any } 372 | static names(): { [key: string]: string } { 373 | return { 374 | map: 'map' 375 | }; 376 | } 377 | 378 | static types(): { [key: string]: any } { 379 | return { 380 | map: { 'type': 'map', 'keyType': 'string', 'valueType': 'any' } 381 | }; 382 | } 383 | constructor(map: { [key: string]: any }) { 384 | this.map = map; 385 | } 386 | } 387 | class UserInfoResponse extends $dara.Model { 388 | name: string 389 | age: number 390 | strong: boolean 391 | title: string[] 392 | metaInfo: MetaInfo 393 | static names(): { [key: string]: string } { 394 | return { 395 | name: 'name', 396 | title: 'title', 397 | strong: 'strong', 398 | age: 'age', 399 | metaInfo: 'metaInfo', 400 | }; 401 | } 402 | 403 | static types(): { [key: string]: any } { 404 | return { 405 | title: { 'type': 'array', 'itemType': 'string' }, 406 | name: 'string', 407 | age: 'number', 408 | strong: 'boolean', 409 | metaInfo: MetaInfo 410 | }; 411 | } 412 | 413 | constructor(map: { [key: string]: any }) { 414 | super(map); 415 | } 416 | } 417 | 418 | assert.throws(function () { 419 | $dara.cast(undefined, new UserInfoResponse({})) 420 | }, function (err: Error) { 421 | assert.strictEqual(err.message, 'can not cast to Map'); 422 | return true; 423 | }); 424 | 425 | assert.throws(function () { 426 | const data = { map: 'string' }; 427 | $dara.cast(data, new MapInfo({})) 428 | }, function (err: Error) { 429 | assert.strictEqual(err.message, 'type of map is mismatch, expect object, but string'); 430 | return true; 431 | }); 432 | 433 | assert.throws(function () { 434 | $dara.cast('data', new UserInfoResponse({})); 435 | }, function (err: Error) { 436 | assert.strictEqual(err.message, 'can not cast to Map'); 437 | return true; 438 | }); 439 | 440 | assert.throws(function () { 441 | const data = { 442 | name: ['123'], 443 | age: 21, 444 | strong: true, 445 | title: ['写代码的'], 446 | metaInfo: new MetaInfo('平台') 447 | } 448 | $dara.cast(data, new UserInfoResponse({})) 449 | }, function (err: Error) { 450 | assert.strictEqual(err.message, 'type of name is mismatch, expect string, but object'); 451 | return true; 452 | }); 453 | 454 | assert.throws(function () { 455 | const data = { 456 | name: '普X', 457 | age: 21, 458 | strong: true, 459 | title: '写代码的', 460 | metaInfo: new MetaInfo('平台') 461 | } 462 | $dara.cast(data, new UserInfoResponse({})); 463 | }, function (err: Error) { 464 | assert.strictEqual(err.message, 'type of title is mismatch, expect array, but string'); 465 | return true; 466 | }); 467 | 468 | assert.throws(function () { 469 | const data = { 470 | name: '普X', 471 | age: '21a', 472 | strong: true, 473 | title: ['写代码的'], 474 | metaInfo: new MetaInfo('平台') 475 | } 476 | $dara.cast(data, new UserInfoResponse({})) 477 | }, function (err: Error) { 478 | assert.strictEqual(err.message, 'type of age is mismatch, expect number, but string') 479 | return true; 480 | }); 481 | 482 | assert.throws(function () { 483 | const data = { 484 | name: '普X', 485 | age: 21, 486 | strong: 'ture', 487 | title: ['写代码的'], 488 | metaInfo: new MetaInfo('平台') 489 | } 490 | $dara.cast(data, new UserInfoResponse({})) 491 | }, function (err: Error) { 492 | assert.strictEqual(err.message, 'type of strong is mismatch, expect boolean, but string') 493 | return true; 494 | }); 495 | 496 | assert.throws(function () { 497 | const data = { 498 | name: '普X', 499 | age: 21, 500 | strong: true, 501 | title: ['写代码的'], 502 | metaInfo: '平台' 503 | } 504 | $dara.cast(data, new UserInfoResponse({})) 505 | }, function (err: Error) { 506 | assert.strictEqual(err.message, 'type of metaInfo is mismatch, expect object, but string') 507 | return true; 508 | }); 509 | }); 510 | 511 | it('cast should ok(with bytes)', function () { 512 | class BytesModel extends $dara.Model { 513 | bytes: Buffer; 514 | static names(): { [key: string]: string } { 515 | return { 516 | bytes: 'bytes', 517 | }; 518 | } 519 | 520 | static types(): { [key: string]: any } { 521 | return { 522 | bytes: 'Buffer', 523 | }; 524 | } 525 | 526 | constructor(map: { [key: string]: any }) { 527 | super(map); 528 | } 529 | } 530 | 531 | const response = $dara.cast({ 532 | bytes: Buffer.from('bytes') 533 | }, new BytesModel({})); 534 | 535 | assert.deepStrictEqual(response.bytes, Buffer.from('bytes')); 536 | }); 537 | 538 | it('cast should ok(with big number)', function () { 539 | class bigNumModel extends $dara.Model { 540 | num: number; 541 | str: string; 542 | static names(): { [key: string]: string } { 543 | return { 544 | num: 'num', 545 | str: 'str', 546 | }; 547 | } 548 | 549 | static types(): { [key: string]: any } { 550 | return { 551 | num: 'number', 552 | str: 'string', 553 | }; 554 | } 555 | 556 | constructor(map: { [key: string]: any }) { 557 | super(map); 558 | } 559 | } 560 | 561 | const response = $dara.cast({ 562 | num: '9007199254740991', 563 | str: 9007199254740991, 564 | }, new bigNumModel({})); 565 | 566 | assert.deepStrictEqual(response, new bigNumModel({ 567 | num: 9007199254740991, 568 | str: '9007199254740991', 569 | })); 570 | }); 571 | }); 572 | 573 | it('retryError should ok', function () { 574 | const err = $dara.retryError(new $dara.Request(), null); 575 | assert.strictEqual(err.name, 'RetryError'); 576 | }); 577 | 578 | it('readable with string should ok', async function () { 579 | const readable = new $dara.BytesReadable('string'); 580 | const buffer = await read(readable); 581 | assert.strictEqual(buffer.toString(), 'string'); 582 | }); 583 | 584 | it('readable with buffer should ok', async function () { 585 | const readable = new $dara.BytesReadable(Buffer.from('string')); 586 | const buffer = await read(readable); 587 | assert.strictEqual(buffer.toString(), 'string'); 588 | }); 589 | 590 | it('isRetryable should ok', function () { 591 | assert.strictEqual($dara.isRetryable(undefined), false); 592 | assert.strictEqual($dara.isRetryable(null), false); 593 | assert.strictEqual($dara.isRetryable(new Error('')), false); 594 | const err = $dara.retryError(new $dara.Request(), null); 595 | assert.strictEqual($dara.isRetryable(err), true); 596 | }); 597 | 598 | it('allowRetry should ok', function () { 599 | // first time to call allowRetry, return true 600 | assert.strictEqual($dara.allowRetry({}, 0, Date.now()), true); 601 | assert.strictEqual($dara.allowRetry({ 602 | retryable: false 603 | }, 1, Date.now()), false); 604 | // never policy 605 | assert.strictEqual($dara.allowRetry({ 606 | retryable: true, 607 | policy: 'never' 608 | }, 1, Date.now()), false); 609 | // always policy 610 | assert.strictEqual($dara.allowRetry({ 611 | retryable: true, 612 | policy: 'always' 613 | }, 1, Date.now()), true); 614 | // simple policy 615 | assert.strictEqual($dara.allowRetry({ 616 | retryable: true, 617 | policy: 'simple', 618 | maxAttempts: 3 619 | }, 1, Date.now()), true); 620 | assert.strictEqual($dara.allowRetry({ 621 | retryable: true, 622 | policy: 'simple', 623 | maxAttempts: 3 624 | }, 3, Date.now()), false); 625 | // timeout 626 | assert.strictEqual($dara.allowRetry({ 627 | retryable: true, 628 | policy: 'timeout', 629 | timeout: 10 630 | }, 1, Date.now() - 100), false); 631 | assert.strictEqual($dara.allowRetry({ 632 | retryable: true, 633 | policy: 'timeout', 634 | timeout: 10 635 | }, 1, Date.now() - 5), true); 636 | // default 637 | assert.strictEqual($dara.allowRetry({ 638 | retryable: true 639 | }, 1, Date.now()), false); 640 | assert.strictEqual($dara.allowRetry({ 641 | retryable: true, 642 | maxAttempts: 'no' 643 | }, 1, Date.now()), false); 644 | assert.strictEqual($dara.allowRetry({ 645 | retryable: true, 646 | maxAttempts: true 647 | }, 1, Date.now()), false); 648 | assert.strictEqual($dara.allowRetry({ 649 | retryable: true, 650 | maxAttempts: 1 651 | }, 1, Date.now()), true); 652 | assert.strictEqual($dara.allowRetry({ 653 | retryable: true, 654 | maxAttempts: 0 655 | }, 1, Date.now()), false); 656 | }); 657 | 658 | it('getBackoffTime should ok', function () { 659 | // first time 660 | assert.strictEqual($dara.getBackoffTime({}, 0), 0); 661 | // no policy 662 | assert.strictEqual($dara.getBackoffTime({ 663 | policy: 'no' 664 | }, 1), 0); 665 | 666 | // fixed policy 667 | assert.strictEqual($dara.getBackoffTime({ 668 | policy: 'fixed', 669 | period: 100 670 | }, 1), 100); 671 | 672 | // random policy 673 | const time = $dara.getBackoffTime({ 674 | policy: 'random', 675 | minPeriod: 10, 676 | maxPeriod: 100 677 | }, 1); 678 | assert.strictEqual(time >= 10 && time <= 100, true); 679 | 680 | // exponential policy 681 | // 1 time 682 | assert.strictEqual($dara.getBackoffTime({ 683 | policy: 'exponential', 684 | initial: 10, 685 | max: 100, 686 | multiplier: 1 687 | }, 1), 10); 688 | // 2 time 689 | assert.strictEqual($dara.getBackoffTime({ 690 | policy: 'exponential', 691 | initial: 10, 692 | max: 100, 693 | multiplier: 1 694 | }, 2), 20); 695 | assert.strictEqual($dara.getBackoffTime({ 696 | policy: 'exponential', 697 | initial: 10, 698 | max: 100, 699 | multiplier: 1 700 | }, 3), 40); 701 | assert.strictEqual($dara.getBackoffTime({ 702 | policy: 'exponential', 703 | initial: 10, 704 | max: 100, 705 | multiplier: 1 706 | }, 4), 80); 707 | assert.strictEqual($dara.getBackoffTime({ 708 | policy: 'exponential', 709 | initial: 10, 710 | max: 100, 711 | multiplier: 1 712 | }, 5), 100); 713 | // exponential_random policy 714 | // 1 time 715 | let b = $dara.getBackoffTime({ 716 | policy: 'exponential_random', 717 | initial: 10, 718 | max: 100, 719 | multiplier: 1 720 | }, 1); 721 | assert.strictEqual(b >= 5 && b <= 15, true); 722 | // 2 time 723 | b = $dara.getBackoffTime({ 724 | policy: 'exponential_random', 725 | initial: 10, 726 | max: 100, 727 | multiplier: 1 728 | }, 2); 729 | assert.strictEqual(b >= 10 && b <= 30, true); 730 | b = $dara.getBackoffTime({ 731 | policy: 'exponential_random', 732 | initial: 10, 733 | max: 100, 734 | multiplier: 1 735 | }, 3); 736 | assert.strictEqual(b >= 20 && b <= 60, true); 737 | b = $dara.getBackoffTime({ 738 | policy: 'exponential_random', 739 | initial: 10, 740 | max: 100, 741 | multiplier: 1 742 | }, 4); 743 | assert.strictEqual(b >= 40 && b <= 100, true); 744 | b = $dara.getBackoffTime({ 745 | policy: 'exponential_random', 746 | initial: 10, 747 | max: 100, 748 | multiplier: 1 749 | }, 5); 750 | assert.strictEqual(b, 100); 751 | 752 | // default 753 | assert.strictEqual($dara.getBackoffTime({ 754 | }, 5), 0); 755 | }); 756 | 757 | it('new Model should ok', function () { 758 | class SubModel extends $dara.Model { 759 | status?: number 760 | bytes?: Readable 761 | file?: Writable 762 | static names(): { [key: string]: string } { 763 | return { 764 | status: 'status', 765 | bytes: 'bytes', 766 | file: 'file', 767 | }; 768 | } 769 | 770 | static types(): { [key: string]: any } { 771 | return { 772 | status: 'number', 773 | bytes: 'Readable', 774 | file: 'Writable' 775 | }; 776 | } 777 | 778 | constructor(map: { [key: string]: any }) { 779 | super(map); 780 | } 781 | } 782 | class MyModel extends $dara.Model { 783 | avatar?: string 784 | role?: string[] 785 | status: SubModel 786 | static names(): { [key: string]: string } { 787 | return { 788 | avatar: 'avatar', 789 | status: 'status', 790 | role: 'role', 791 | }; 792 | } 793 | 794 | static types(): { [key: string]: any } { 795 | return { 796 | avatar: 'string', 797 | status: SubModel, 798 | role: { type: 'array', itemType: 'string' } 799 | }; 800 | } 801 | 802 | constructor(map: { [key: string]: any }) { 803 | super(map); 804 | } 805 | } 806 | 807 | let m = new MyModel(null); 808 | assert.strictEqual(m.avatar, undefined); 809 | assert.strictEqual($dara.toMap(m)['avatar'], undefined); 810 | assert.strictEqual($dara.toMap(), null); 811 | assert.strictEqual($dara.toMap(undefined), null); 812 | assert.strictEqual($dara.toMap(null), null); 813 | 814 | m = new MyModel({ avatar: 'avatar url' }); 815 | assert.strictEqual(m.avatar, 'avatar url'); 816 | assert.strictEqual($dara.toMap(m)['avatar'], 'avatar url'); 817 | 818 | m = new MyModel({ 819 | avatar: 'avatar url', 820 | role: ['admin', 'user'], 821 | }); 822 | assert.strictEqual($dara.toMap(m)['role'][0], 'admin'); 823 | assert.strictEqual($dara.toMap(m)['role'][1], 'user'); 824 | const testReadalbe = new $dara.BytesReadable('test'); 825 | const testWritable = new Writable(); 826 | m = new MyModel({ 827 | status: new SubModel({ 828 | status: 1, 829 | bytes: testReadalbe, 830 | file: testWritable, 831 | }) 832 | }); 833 | assert.strictEqual($dara.toMap(m)['status']['status'], 1); 834 | assert.strictEqual($dara.toMap(m)['status']['bytes'], testReadalbe); 835 | assert.strictEqual($dara.toMap(m)['status']['file'], testWritable); 836 | 837 | assert.strictEqual($dara.toMap(m, true)['status']['status'], 1); 838 | assert.strictEqual($dara.toMap(m, true)['status']['bytes'], null); 839 | assert.strictEqual($dara.toMap(m, true)['status']['file'], null); 840 | }); 841 | 842 | it('new Model with wrong type should error', function () { 843 | class MyModel extends $dara.Model { 844 | avatar?: string 845 | role?: string[] 846 | static names(): { [key: string]: string } { 847 | return { 848 | avatar: 'avatar', 849 | role: 'role', 850 | }; 851 | } 852 | 853 | static types(): { [key: string]: any } { 854 | return { 855 | avatar: 'string', 856 | role: { type: 'array', itemType: 'string' } 857 | }; 858 | } 859 | 860 | constructor(map: { [key: string]: any }) { 861 | super(map); 862 | } 863 | } 864 | assert.throws(function () { 865 | const m = new MyModel({ 866 | avatar: 'avatar url', 867 | role: 'admin', 868 | }); 869 | }, function (err: Error) { 870 | assert.strictEqual(err.message, 'expect: array, actual: string'); 871 | return true; 872 | }); 873 | }); 874 | 875 | it('Model function should ok', async function () { 876 | class SubModel extends $dara.Model { 877 | status: number 878 | bytes?: Readable 879 | file?: Writable 880 | 881 | validate() { 882 | $dara.Model.validateRequired('status', this.status); 883 | $dara.Model.validateMaximum('status', this.status, 300); 884 | $dara.Model.validateMinimum('status', this.status, 100); 885 | } 886 | 887 | static names(): { [key: string]: string } { 888 | return { 889 | status: 'status', 890 | bytes: 'bytes', 891 | file: 'file', 892 | }; 893 | } 894 | 895 | static types(): { [key: string]: any } { 896 | return { 897 | status: 'number', 898 | bytes: 'Readable', 899 | file: 'Writable' 900 | }; 901 | } 902 | 903 | constructor(map: { [key: string]: any }) { 904 | super(map); 905 | } 906 | } 907 | class MyModel extends $dara.Model { 908 | avatar?: string 909 | role?: string[] 910 | status: SubModel 911 | static names(): { [key: string]: string } { 912 | return { 913 | avatar: 'avatar', 914 | status: 'status', 915 | role: 'role', 916 | }; 917 | } 918 | 919 | static types(): { [key: string]: any } { 920 | return { 921 | avatar: 'string', 922 | status: SubModel, 923 | role: { type: 'array', itemType: 'string' } 924 | }; 925 | } 926 | 927 | validate() { 928 | $dara.Model.validateRequired('status', this.status) 929 | $dara.Model.validateMaxLength('role', this.role, 5); 930 | $dara.Model.validateMinLength('role', this.role, 1); 931 | $dara.Model.validatePattern("avatar", this.avatar, "^https://"); 932 | super.validate(); 933 | } 934 | 935 | constructor(map: { [key: string]: any }) { 936 | super(map); 937 | } 938 | } 939 | 940 | const testReadalbe = new $dara.BytesReadable('test'); 941 | const testWritable = new Writable(); 942 | let m = new MyModel({ 943 | avatar: 'https://avatarurl.com/path', 944 | role: ['admin', 'user'], 945 | status: new SubModel({ 946 | status: 101, 947 | bytes: testReadalbe, 948 | file: testWritable, 949 | }) 950 | }); 951 | assert.doesNotThrow(m.validate.bind(m), $dara.BaseError); 952 | assert.deepStrictEqual(m.copyWithoutStream().toMap(), { 953 | avatar: 'https://avatarurl.com/path', 954 | role: ['admin', 'user'], 955 | status: { 956 | status: 101, 957 | } 958 | }); 959 | 960 | assert.throws(function () { 961 | const m = new MyModel({ 962 | avatar: 'avatarurl', 963 | status: new SubModel({ 964 | status: 101, 965 | }) 966 | }); 967 | m.validate(); 968 | }, function (err: $dara.BaseError) { 969 | assert.strictEqual(err.message, 'SDK.ValidateError: avatar is not match ^https://.'); 970 | return true; 971 | }); 972 | 973 | assert.throws(function () { 974 | const m = new MyModel({ 975 | role: [], 976 | status: new SubModel({ 977 | status: 101, 978 | }) 979 | }); 980 | m.validate(); 981 | }, function (err: $dara.BaseError) { 982 | assert.strictEqual(err.message, 'SDK.ValidateError: role is exceed min-length: 1.'); 983 | return true; 984 | }); 985 | 986 | assert.throws(function () { 987 | const m = new MyModel({ 988 | avatar: 'https://avatarurl.com/path', 989 | role: ['a', 'b', 'c', 'd', 'e', 'f'], 990 | status: new SubModel({ 991 | status: 101, 992 | bytes: testReadalbe, 993 | file: testWritable, 994 | }) 995 | }); 996 | m.validate(); 997 | }, function (err: $dara.BaseError) { 998 | assert.strictEqual(err.message, 'SDK.ValidateError: role is exceed max-length: 5.'); 999 | return true; 1000 | }); 1001 | 1002 | assert.throws(function () { 1003 | const m = new SubModel({ 1004 | status: 99, 1005 | }); 1006 | m.validate(); 1007 | }, function (err: $dara.BaseError) { 1008 | assert.strictEqual(err.message, 'SDK.ValidateError: status cannot be less than 100.'); 1009 | return true; 1010 | }); 1011 | 1012 | assert.throws(function () { 1013 | const m = new SubModel({ 1014 | status: 301, 1015 | }); 1016 | m.validate(); 1017 | }, function (err: $dara.BaseError) { 1018 | assert.strictEqual(err.message, 'SDK.ValidateError: status cannot be greater than 300.'); 1019 | return true; 1020 | }); 1021 | 1022 | assert.throws(function () { 1023 | const m = new SubModel({ 1024 | bytes: testReadalbe, 1025 | file: testWritable, 1026 | }); 1027 | m.validate(); 1028 | }, function (err: $dara.BaseError) { 1029 | assert.strictEqual(err.message, 'SDK.ValidateError: status is required.'); 1030 | return true; 1031 | }); 1032 | 1033 | class ComplexModel extends $dara.Model { 1034 | modelArr?: MyModel[][] 1035 | modelMap?: { [key: string]: SubModel[] } 1036 | modelMapArr?: { [key: string]: SubModel }[] 1037 | static names(): { [key: string]: string } { 1038 | return { 1039 | modelArr: 'modelArr', 1040 | modelMap: 'modelMap', 1041 | modelMapArr: 'modelMapArr', 1042 | }; 1043 | } 1044 | 1045 | static types(): { [key: string]: any } { 1046 | return { 1047 | modelArr: { type: 'array', itemType: { type: 'array', itemType: MyModel } }, 1048 | modelMap: { type: 'map', 'keyType': 'string', 'valueType': { type: 'array', itemType: SubModel } }, 1049 | modelMapArr: { type: 'array', itemType: { type: 'map', 'keyType': 'string', 'valueType': SubModel } }, 1050 | }; 1051 | } 1052 | 1053 | validate() { 1054 | if(Array.isArray(this.modelArr)) { 1055 | $dara.Model.validateArray(this.modelArr); 1056 | } 1057 | if(this.modelMap) { 1058 | $dara.Model.validateMap(this.modelMap); 1059 | } 1060 | if(Array.isArray(this.modelMapArr)) { 1061 | $dara.Model.validateArray(this.modelMapArr); 1062 | } 1063 | super.validate(); 1064 | } 1065 | } 1066 | m = new MyModel({ 1067 | avatar: 'https://avatarurl.com/path', 1068 | role: ['admin', 'user'], 1069 | status: new SubModel({ 1070 | status: 101, 1071 | bytes: testReadalbe, 1072 | file: testWritable, 1073 | }) 1074 | }); 1075 | let subM = new SubModel({ 1076 | status: 101, 1077 | bytes: testReadalbe, 1078 | file: testWritable, 1079 | }); 1080 | let cm = new ComplexModel({ 1081 | modelArr: [[m]], 1082 | modelMap: { 1083 | key: [subM], 1084 | key1: undefined, 1085 | }, 1086 | modelMapArr: [null] 1087 | }); 1088 | assert.doesNotThrow(cm.validate.bind(cm), $dara.BaseError); 1089 | 1090 | assert.throws(function () { 1091 | const m = new MyModel({ 1092 | avatar: 'avatarurl', 1093 | status: new SubModel({ 1094 | status: 101, 1095 | }) 1096 | }); 1097 | const subM = new SubModel({ 1098 | status: 101, 1099 | bytes: testReadalbe, 1100 | file: testWritable, 1101 | }); 1102 | const cm = new ComplexModel({ 1103 | modelArr: [[m]], 1104 | modelMap: { 1105 | key: [subM], 1106 | key1: undefined, 1107 | }, 1108 | modelMapArr: [] 1109 | }); 1110 | cm.validate(); 1111 | }, function (err: $dara.BaseError) { 1112 | assert.strictEqual(err.message, 'SDK.ValidateError: avatar is not match ^https://.'); 1113 | return true; 1114 | }); 1115 | 1116 | 1117 | assert.throws(function () { 1118 | const subM = new SubModel({ 1119 | status: 301, 1120 | }); 1121 | const cm = new ComplexModel({ 1122 | modelMap: { 1123 | key: [subM], 1124 | key1: undefined, 1125 | }, 1126 | modelMapArr: [] 1127 | }); 1128 | cm.validate(); 1129 | }, function (err: $dara.BaseError) { 1130 | assert.strictEqual(err.message, 'SDK.ValidateError: status cannot be greater than 300.'); 1131 | return true; 1132 | }); 1133 | 1134 | assert.throws(function () { 1135 | const subM = new SubModel({ 1136 | bytes: testReadalbe, 1137 | file: testWritable, 1138 | }); 1139 | const cm = new ComplexModel({ 1140 | modelMapArr: [{ 1141 | key: subM 1142 | }] 1143 | }); 1144 | cm.validate(); 1145 | }, function (err: $dara.BaseError) { 1146 | assert.strictEqual(err.message, 'SDK.ValidateError: status is required.'); 1147 | return true; 1148 | }); 1149 | }); 1150 | 1151 | it('sleep should ok', async function () { 1152 | const start = Date.now(); 1153 | await $dara.sleep(10); 1154 | assert.ok(Date.now() - start >= 10); 1155 | }); 1156 | 1157 | it('doAction should ok', async function () { 1158 | const request = new $dara.Request(); 1159 | request.pathname = '/'; 1160 | request.port = (server.address() as AddressInfo).port; 1161 | request.headers['host'] = '127.0.0.1'; 1162 | request.query = { id: '1' }; 1163 | const res = await $dara.doAction(request, { timeout: 1000, ignoreSSL: true }); 1164 | assert.strictEqual(res.statusCode, 200); 1165 | const bytes = await res.readBytes(); 1166 | assert.strictEqual(bytes.toString(), 'Hello world!'); 1167 | }); 1168 | 1169 | it('doAction when path with query should ok', async function () { 1170 | const request = new $dara.Request(); 1171 | request.pathname = '/?name'; 1172 | request.port = (server.address() as AddressInfo).port; 1173 | request.headers['host'] = '127.0.0.1'; 1174 | request.query = { id: '1' }; 1175 | const res = await $dara.doAction(request, { timeout: 1000, ignoreSSL: true }); 1176 | assert.strictEqual(res.statusCode, 200); 1177 | const bytes = await res.readBytes(); 1178 | assert.strictEqual(bytes.toString(), 'Hello world!'); 1179 | }); 1180 | 1181 | it('doAction with post method should ok', async function () { 1182 | const request = new $dara.Request(); 1183 | request.method = 'POST'; 1184 | request.pathname = '/'; 1185 | request.port = (server.address() as AddressInfo).port; 1186 | request.headers['host'] = '127.0.0.1'; 1187 | const res = await $dara.doAction(request, { timeout: 1000, ignoreSSL: true }); 1188 | assert.strictEqual(res.statusCode, 200); 1189 | const bytes = await res.readBytes(); 1190 | assert.strictEqual(bytes.toString(), 'Hello world!'); 1191 | }); 1192 | 1193 | it('doAction with self-signed certificates should ok', async function () { 1194 | const request = new $dara.Request(); 1195 | request.method = 'POST'; 1196 | request.pathname = '/'; 1197 | request.port = (server.address() as AddressInfo).port; 1198 | request.headers['host'] = '127.0.0.1'; 1199 | const res = await $dara.doAction(request, { 1200 | timeout: 1000, 1201 | ignoreSSL: true, 1202 | key: 'private rsa key', 1203 | cert: 'private certification', 1204 | }); 1205 | assert.strictEqual(res.statusCode, 200); 1206 | const bytes = await res.readBytes(); 1207 | assert.strictEqual(bytes.toString(), 'Hello world!'); 1208 | }); 1209 | 1210 | it('doAction with ca should ok', async function () { 1211 | const request = new $dara.Request(); 1212 | request.method = 'POST'; 1213 | request.pathname = '/'; 1214 | request.port = (server.address() as AddressInfo).port; 1215 | request.headers['host'] = '127.0.0.1'; 1216 | const res = await $dara.doAction(request, { 1217 | timeout: 1000, 1218 | ignoreSSL: true, 1219 | ca: 'ca', 1220 | }); 1221 | assert.strictEqual(res.statusCode, 200); 1222 | const bytes = await res.readBytes(); 1223 | assert.strictEqual(bytes.toString(), 'Hello world!'); 1224 | }); 1225 | 1226 | it('doAction with timeout should ok', async function () { 1227 | const request = new $dara.Request(); 1228 | request.method = 'POST'; 1229 | request.pathname = '/timeout'; 1230 | request.port = (server.address() as AddressInfo).port; 1231 | request.headers['host'] = '127.0.0.1'; 1232 | const res = await $dara.doAction(request, { 1233 | connectTimeout: 6000, 1234 | readTimeout: 6000 1235 | }); 1236 | assert.strictEqual(res.statusCode, 200); 1237 | const bytes = await res.readBytes(); 1238 | assert.strictEqual(bytes.toString(), 'Hello world!'); 1239 | }); 1240 | 1241 | it('doAction with keepAlive should ok', async function () { 1242 | const request = new $dara.Request(); 1243 | request.method = 'POST'; 1244 | request.pathname = '/keepAlive'; 1245 | request.port = (server.address() as AddressInfo).port; 1246 | request.headers['host'] = '127.0.0.1'; 1247 | let res = await $dara.doAction(request); 1248 | assert.strictEqual(res.statusCode, 200); 1249 | assert.strictEqual(res.headers['client-keep-alive'], 'keep-alive'); 1250 | let bytes = await res.readBytes(); 1251 | assert.strictEqual(bytes.toString(), 'Hello world!'); 1252 | 1253 | res = await $dara.doAction(request, { 1254 | keepAlive: true 1255 | }); 1256 | assert.strictEqual(res.statusCode, 200); 1257 | assert.strictEqual(res.headers['client-keep-alive'], 'keep-alive'); 1258 | bytes = await res.readBytes(); 1259 | assert.strictEqual(bytes.toString(), 'Hello world!'); 1260 | 1261 | res = await $dara.doAction(request, { 1262 | keepAlive: false 1263 | }); 1264 | assert.strictEqual(res.statusCode, 200); 1265 | assert.strictEqual(res.headers['client-keep-alive'], 'close'); 1266 | bytes = await res.readBytes(); 1267 | assert.strictEqual(bytes.toString(), 'Hello world!'); 1268 | }); 1269 | 1270 | it('toMap another version model', async function () { 1271 | class AnotherModel { 1272 | toMap(): { [key: string]: any } { 1273 | return {}; 1274 | } 1275 | } 1276 | 1277 | const m = new AnotherModel(); 1278 | assert.deepStrictEqual($dara.toMap(m), {}); 1279 | }); 1280 | }); --------------------------------------------------------------------------------