├── .eslintrc.json ├── .github └── workflows │ └── npmpublish.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── command │ └── index.ts ├── index.ts ├── server │ ├── index.ts │ ├── lib │ │ ├── decorator.ts │ │ ├── error.ts │ │ ├── httpbase.ts │ │ ├── request.ts │ │ ├── response.ts │ │ ├── rpc.ts │ │ └── util.ts │ ├── middleware │ │ ├── outputCatch.ts │ │ └── start.ts │ ├── mini │ │ ├── app.ts │ │ ├── index.ts │ │ ├── router.ts │ │ └── websocket.ts │ └── worker │ │ ├── pool.ts │ │ ├── worker.ts │ │ ├── workerManage.ts │ │ └── workerStream.ts ├── test │ └── server.test.ts ├── types │ └── server.ts └── util.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": "standard-with-typescript", 7 | "parserOptions": { 8 | "ecmaVersion": "latest" 9 | }, 10 | "rules": { 11 | "@typescript-eslint/strict-boolean-expressions":"off", 12 | "@typescript-eslint/explicit-function-return-type":"off", 13 | "@typescript-eslint/no-misused-promises":"off", 14 | "@typescript-eslint/ban-types":"off", 15 | "@typescript-eslint/no-var-requires": "off", 16 | "@typescript-eslint/no-unsafe-argument": "off", 17 | "@typescript-eslint/no-floating-promises": "off", 18 | "@typescript-eslint/prefer-nullish-coalescing": "off", 19 | "@typescript-eslint/no-extraneous-class": "off", 20 | "@typescript-eslint/ban-ts-comment": "off", 21 | "new-cap":"off", 22 | "no-return-assign": 0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | - run: npm ci 17 | - run: npm test 18 | 19 | publish-npm: 20 | needs: build 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v1 24 | - uses: actions/setup-node@v1 25 | with: 26 | node-version: 12 27 | registry-url: https://registry.npmjs.org/ 28 | - run: npm ci 29 | - run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # project 107 | build/ 108 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 virtual-less 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vaas-framework 2 | Virtual as a Service Framework 3 | 4 | # Structure 5 | ![Structure](https://raw.githubusercontent.com/virtual-less/assets/main/vaas-framework.png) 6 | 7 | # Quick Start 8 | Quick init vaas project command: 9 | ```sh 10 | npm init vaas 11 | ``` 12 | ## simple app base code 13 | ```ts 14 | // # src/apps/mini/index.ts 15 | import {VaasServerType, Decorator } from 'vaas-framework' 16 | 17 | export default class Mini { 18 | @Decorator.VaasServer({type:'http',method:'get',routerName:'/hello'}) 19 | async hello({req,res}:VaasServerType.HttpParams) { 20 | return { 21 | hello:'world' 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | 28 | # api doc 29 | ## vaas.config.js 30 | ```ts 31 | export interface VaasConfig { 32 | appsDir:string, 33 | port:number, 34 | getAppNameByRequest:GetAppNameByRequest, 35 | getAppConfigByAppName:GetAppConfigByAppName, 36 | showErrorStack:boolean 37 | isPrepareWorker:boolean 38 | } 39 | ``` 40 | * type GetAppNameByRequest 41 | ```ts 42 | export interface GetAppNameByRequest { 43 | (request:Koa.Request): Promise; 44 | } 45 | ``` 46 | * type GetAppConfigByAppName 47 | ```ts 48 | export interface GetAppConfigByAppName { 49 | (appName:string): Promise; 50 | } 51 | ``` 52 | * type AppConfig 53 | ```ts 54 | export interface AppConfig { 55 | maxWorkerNum:number, 56 | allowModuleSet:Set, 57 | timeout:number, 58 | resourceLimits?:ResourceLimits 59 | } 60 | interface ResourceLimits { 61 | /** 62 | * The maximum size of a heap space for recently created objects. 63 | */ 64 | maxYoungGenerationSizeMb?: number | undefined; 65 | /** 66 | * The maximum size of the main heap in MB. 67 | */ 68 | maxOldGenerationSizeMb?: number | undefined; 69 | /** 70 | * The size of a pre-allocated memory range used for generated code. 71 | */ 72 | codeRangeSizeMb?: number | undefined; 73 | /** 74 | * The default maximum stack size for the thread. Small values may lead to unusable Worker instances. 75 | * @default 4 76 | */ 77 | stackSizeMb?: number | undefined; 78 | } 79 | ``` 80 | ## Decorator.VaasServer 81 | ```ts 82 | export function VaasServer(vaasServer:ServerValue={type:'http'}) 83 | ``` 84 | * type ServerValue 85 | ```ts 86 | export interface ServerValue { 87 | type:ServerType, 88 | method?: 'get' | 'post' | 'put' | 'delete' | 'patch'| 'options'; 89 | routerName?: string; 90 | } 91 | ``` 92 | routerName will be translated to regular expressions using [path-to-regexp](https://github.com/pillarjs/path-to-regexp). 93 | ## req&res 94 | ```ts 95 | export interface HttpParams { 96 | req: RequestConfig; 97 | res: ResponseConfig; 98 | } 99 | ``` 100 | * type RequestConfig 101 | ```ts 102 | export interface RequestConfig { 103 | /** 104 | * Get the charset when present or undefined. 105 | */ 106 | readonly charset: string; 107 | /** 108 | * Return parsed Content-Length when present. 109 | */ 110 | readonly length: number; 111 | /** 112 | * Return the request mime type void of 113 | * parameters such as "charset". 114 | */ 115 | readonly type: string; 116 | /** 117 | * Return request header, alias as request.header 118 | */ 119 | readonly headers: NodeJS.Dict; 120 | /** 121 | * Get request body. 122 | */ 123 | readonly body?: Record; 124 | /** 125 | * Get query string. 126 | */ 127 | readonly rawBody: string; 128 | /** 129 | * Get/Set request URL. 130 | */ 131 | url: string; 132 | /** 133 | * Get origin of URL. 134 | */ 135 | readonly origin: string; 136 | /** 137 | * Get full request URL. 138 | */ 139 | readonly href: string; 140 | /** 141 | * Get/Set request method. 142 | */ 143 | method: string; 144 | /** 145 | * Get request pathname. 146 | * Set pathname, retaining the query-string when present. 147 | */ 148 | path: string; 149 | /** 150 | * Get parsed routerName-params. 151 | * Set routerName-params as an object. 152 | */ 153 | params: NodeJS.Dict; 154 | /** 155 | * Get parsed query-string. 156 | * Set query-string as an object. 157 | */ 158 | query: NodeJS.Dict; 159 | /** 160 | * Get/Set query string. 161 | */ 162 | querystring: string; 163 | /** 164 | * Get the search string. Same as the querystring 165 | * except it includes the leading ?. 166 | * 167 | * Set the search string. Same as 168 | * response.querystring= but included for ubiquity. 169 | */ 170 | search: string; 171 | /** 172 | * Parse the "Host" header field host 173 | * and support X-Forwarded-Host when a 174 | * proxy is enabled. 175 | */ 176 | readonly host: string; 177 | /** 178 | * Parse the "Host" header field hostname 179 | * and support X-Forwarded-Host when a 180 | * proxy is enabled. 181 | */ 182 | readonly hostname: string; 183 | /** 184 | * Check if the request is fresh, aka 185 | * Last-Modified and/or the ETag 186 | * still match. 187 | */ 188 | readonly fresh: boolean; 189 | /** 190 | * Check if the request is stale, aka 191 | * "Last-Modified" and / or the "ETag" for the 192 | * resource has changed. 193 | */ 194 | readonly stale: boolean; 195 | /** 196 | * Check if the request is idempotent. 197 | */ 198 | readonly idempotent: boolean; 199 | /** 200 | * Return the protocol string "http" or "https" 201 | * when requested with TLS. When the proxy setting 202 | * is enabled the "X-Forwarded-Proto" header 203 | * field will be trusted. If you're running behind 204 | * a reverse proxy that supplies https for you this 205 | * may be enabled. 206 | */ 207 | readonly protocol: string; 208 | /** 209 | * Short-hand for: 210 | * 211 | * this.protocol == 'https' 212 | */ 213 | readonly secure: boolean; 214 | /** 215 | * Request remote address. Supports X-Forwarded-For when app.proxy is true. 216 | */ 217 | readonly ip: string; 218 | /** 219 | * When `app.proxy` is `true`, parse 220 | * the "X-Forwarded-For" ip address list. 221 | * 222 | * For example if the value were "client, proxy1, proxy2" 223 | * you would receive the array `["client", "proxy1", "proxy2"]` 224 | * where "proxy2" is the furthest down-stream. 225 | */ 226 | readonly ips: string[]; 227 | } 228 | ``` 229 | * type ResponseConfig 230 | ```ts 231 | export interface ResponseConfig { 232 | /** 233 | * Return response header. 234 | */ 235 | headers: NodeJS.Dict; 236 | /** 237 | * Get/Set response status code. 238 | */ 239 | status: number; 240 | /** 241 | * Get response status message 242 | */ 243 | readonly message: string; 244 | /** 245 | * Return parsed response Content-Length when present. 246 | * Set Content-Length field to `n`. 247 | */ 248 | length: number; 249 | /** 250 | * Return the response mime type void of 251 | * parameters such as "charset". 252 | * 253 | * Set Content-Type response header with `type` through `mime.lookup()` 254 | * when it does not contain a charset. 255 | * 256 | * Examples: 257 | * 258 | * this.type = '.html'; 259 | * this.type = 'html'; 260 | * this.type = 'json'; 261 | * this.type = 'application/json'; 262 | * this.type = 'png'; 263 | */ 264 | type: string; 265 | /** 266 | * Get the Last-Modified date in Date form, if it exists. 267 | * Set the Last-Modified date using a string or a Date. 268 | * 269 | * this.response.lastModified = new Date(); 270 | * this.response.lastModified = '2013-09-13'; 271 | */ 272 | lastModified: Date; 273 | /** 274 | * Get/Set the ETag of a response. 275 | * This will normalize the quotes if necessary. 276 | * 277 | * this.response.etag = 'md5hashsum'; 278 | * this.response.etag = '"md5hashsum"'; 279 | * this.response.etag = 'W/"123456789"'; 280 | * 281 | * @param {String} etag 282 | * @api public 283 | */ 284 | etag: string; 285 | } 286 | ``` 287 | 288 | ## rpc.rpcInvote 289 | ```ts 290 | export async function rpcInvote(appServerName:string,params:P):Promise 291 | ``` 292 | appServerName is appName.serverName -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vaas-framework", 3 | "version": "1.9.16", 4 | "description": "Virtual as a Service Framework", 5 | "main": "./build/index.js", 6 | "bin": { 7 | "vaas": "./build/command/index.js" 8 | }, 9 | "scripts": { 10 | "build": "rm -rf ./build/ && npx tsc", 11 | "test": "npm run build && npx mocha ./build/test/*.test.js", 12 | "prepublish": "npm run build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/virtual-less/vaas-framework.git" 17 | }, 18 | "keywords": [ 19 | "virtual", 20 | "service", 21 | "server", 22 | "less", 23 | "framework" 24 | ], 25 | "author": "zy445566", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/virtual-less/vaas-framework/issues" 29 | }, 30 | "homepage": "https://github.com/virtual-less/vaas-framework#readme", 31 | "dependencies": { 32 | "@types/koa": "^2.13.5", 33 | "@types/koa-bodyparser": "^4.3.8", 34 | "@types/koa-router": "^7.4.8", 35 | "@types/node": "^18.7.21", 36 | "commander": "^9.4.1", 37 | "compressing": "^1.6.2", 38 | "koa": "^2.13.4", 39 | "koa-bodyparser": "^4.3.0", 40 | "koa-router": "^12.0.1", 41 | "node-fetch": "^2.6.7", 42 | "path-to-regexp": "^6.2.1", 43 | "reflect-metadata": "^0.2.1", 44 | "uuid": "^9.0.0", 45 | "vaas-core": "^1.2.2", 46 | "ws": "^8.10.0" 47 | }, 48 | "devDependencies": { 49 | "@typescript-eslint/eslint-plugin": "^6.13.2", 50 | "eslint": "^8.55.0", 51 | "eslint-config-standard-with-typescript": "^42.0.0", 52 | "eslint-plugin-import": "^2.29.0", 53 | "eslint-plugin-n": "^16.4.0", 54 | "eslint-plugin-promise": "^6.1.1", 55 | "mocha": "^10.0.0", 56 | "typescript": "4.8.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/command/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Command } from 'commander' 3 | import * as path from 'path' 4 | import * as os from 'os' 5 | import fetch from 'node-fetch' 6 | import { promises as fsPromises } from 'fs' 7 | import * as compressing from 'compressing' 8 | 9 | import { VaasServer } from '../server/index' 10 | import { serverEOL } from '../server/lib/util' 11 | 12 | const packageInfo = require('../../package.json') 13 | const program = new Command() 14 | const vaasServer = new VaasServer() 15 | 16 | function getConfig (configPath) { 17 | let defaultAppDir = path.join(process.cwd(), 'build/apps') 18 | const defaultConfigPath = path.join(process.cwd(), 'vaas.config.js') 19 | if (configPath) { 20 | if (!path.isAbsolute(configPath)) { 21 | configPath = path.join(process.cwd(), configPath) 22 | } 23 | } 24 | const tsconfigPath = path.join(process.cwd(), 'tsconfig.json') 25 | try { 26 | const tsconfig = require(tsconfigPath) 27 | const tsOutDir = tsconfig?.compilerOptions?.outDir 28 | if (tsOutDir) { 29 | defaultAppDir = path.join(process.cwd(), tsOutDir, 'apps') 30 | } 31 | } catch (error) {} 32 | const vaasConfig = require(configPath || defaultConfigPath) 33 | const finalyConfig = Object.assign({ 34 | appsDir: defaultAppDir, 35 | port: 8080, 36 | getAppNameByRequest: async (_request) => { 37 | return { appName: '', prefix: '/' } 38 | }, 39 | getAppConfigByAppName: async (_appName) => { 40 | return { 41 | maxWorkerNum: 2, 42 | allowModuleSet: new Set(['*']), 43 | timeout: 30 * 1000, 44 | useVmLoadDependencies: true 45 | } 46 | }, 47 | getByPassFlowVersion: async (_appName) => { 48 | // 如果返回空字符串,则直接读取当前目录 49 | return { version: '' } 50 | }, 51 | showErrorStack: true, 52 | isPrepareWorker: true 53 | }, vaasConfig) 54 | return finalyConfig 55 | } 56 | 57 | program 58 | .name('vaas-cli') 59 | .description('CLI to run vaas project') 60 | .version(packageInfo.version) 61 | .option('-c, --configPath ', 'server config path') 62 | .action(async (options) => { 63 | const config = getConfig(options.configPath) 64 | await vaasServer.run(config) 65 | serverEOL({port: config.port}) 66 | }) 67 | 68 | function getApiJsonError (apiJson) { 69 | if (apiJson.errmsg) { 70 | const error = new Error(apiJson.errmsg) 71 | if (apiJson.stack) { error.stack = apiJson.stack } 72 | console.error(error) 73 | throw error 74 | } 75 | } 76 | 77 | program.command('deploy') 78 | .description('deploy app to platform') 79 | .option('-c, --configPath ', 'server config path') 80 | .option('-h, --platformAddressHost ', 'platform remote address') 81 | .option('-a, --appNames ', 'platform remote address', '*') 82 | .action(async (options) => { 83 | if (!options.platformAddressHost) { 84 | throw new Error('option platformAddressHost can\'t be empty!') 85 | } 86 | const getUploadUrlApi = `${options.platformAddressHost}/getUploadUrl` 87 | const deployApi = `${options.platformAddressHost}/deploy` 88 | const config = getConfig(options.configPath) 89 | const appNameList = await fsPromises.readdir(config.appsDir) 90 | const appNamesList = options.appNames.split(',') 91 | for (const appName of appNameList) { 92 | if (['.', '..', '.DS_Store'].includes(appName)) { continue } 93 | const IsPackageApp = appNamesList.includes('*') || appNamesList.includes(appName) 94 | if (!IsPackageApp) { continue } 95 | const fileName = `${appName}.zip` 96 | const getUploadUrlResp = await fetch(`${getUploadUrlApi}?fileName=${fileName}`) 97 | const getUploadUrlJson = await getUploadUrlResp.json() 98 | getApiJsonError(getUploadUrlJson) 99 | const distAppPath = path.join(config.appsDir, fileName) 100 | const appDirPath = path.join(config.appsDir, appName) 101 | const stat = await fsPromises.stat(appDirPath) 102 | if (!stat.isDirectory()) { continue } 103 | await compressing.zip.compressDir(appDirPath, distAppPath, { 104 | ignoreBase: true 105 | }) 106 | await fetch(getUploadUrlJson.data.uploadUrl, { 107 | method: 'PUT', 108 | body: await fsPromises.readFile(distAppPath) 109 | }) 110 | await fsPromises.unlink(distAppPath) 111 | const appPackageJsonPath = path.join(appDirPath, 'package.json') 112 | const appPackageJsonStat = await fsPromises.stat(appPackageJsonPath) 113 | if (!appPackageJsonStat.isFile()) { 114 | throw new Error(`appDir[${appDirPath}] not have package.json!`) 115 | } 116 | const appPackageJson = require(appPackageJsonPath) 117 | const deployApiResp = await fetch(deployApi, { 118 | method: 'post', 119 | body: JSON.stringify({ 120 | appBuildS3Key: getUploadUrlJson.data.key, 121 | appName, 122 | version: appPackageJson.version 123 | }), 124 | headers: { 'Content-Type': 'application/json' } 125 | }) 126 | const deployApiJson = await deployApiResp.json() 127 | getApiJsonError(deployApiJson) 128 | } 129 | }) 130 | 131 | program.command('help') 132 | .description('help') 133 | .action(() => { 134 | program.help() 135 | }) 136 | 137 | program.parse() 138 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * as VaasServerType from './types/server' 2 | 3 | export * as Decorator from './server/lib/decorator' 4 | 5 | export { rpcInvote } from './server/lib/rpc' 6 | export { VaasServer } from './server/index' 7 | 8 | export { MiniVaasServer } from './server/mini/index' 9 | 10 | export * as util from './util' 11 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa' 2 | import { type Server as HttpServer } from 'http' 3 | 4 | import * as KoaBodyparser from 'koa-bodyparser' 5 | import { outputCatch } from './middleware/outputCatch' 6 | import { httpStart, webSocketStart } from './middleware/start' 7 | import { VaasWorkPool } from './worker/pool' 8 | import { type VaasConfig } from '../types/server' 9 | 10 | export class VaasServer { 11 | server: HttpServer 12 | async run ({ 13 | appsDir, port, 14 | getAppNameByRequest, 15 | getAppConfigByAppName, 16 | getByPassFlowVersion, 17 | showErrorStack, 18 | isPrepareWorker 19 | }: VaasConfig): Promise { 20 | const vaasWorkPool = new VaasWorkPool({ 21 | appsDir, 22 | getAppConfigByAppName, 23 | getByPassFlowVersion 24 | }) 25 | if (isPrepareWorker) { 26 | await vaasWorkPool.prepareWorker() 27 | } 28 | const app = new Koa() 29 | app.use(outputCatch({ showErrorStack })) 30 | app.use(KoaBodyparser({ 31 | formLimit: '30mb', 32 | jsonLimit: '30mb', 33 | textLimit: '30mb', 34 | xmlLimit: '30mb' 35 | })) 36 | app.use(httpStart({ 37 | vaasWorkPool, 38 | getAppNameByRequest, 39 | getByPassFlowVersion 40 | })) 41 | return await new Promise((resolve) => { 42 | this.server = app.listen(port, () => { 43 | webSocketStart({ 44 | app, 45 | server: this.server, 46 | vaasWorkPool, 47 | getAppNameByRequest, 48 | getByPassFlowVersion 49 | }) 50 | return resolve(app) 51 | }) 52 | }) 53 | } 54 | 55 | async close (): Promise { 56 | return await new Promise((resolve, reject) => { 57 | this.server.close((error) => { 58 | if (error)reject(error) 59 | return resolve(true) 60 | }) 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/server/lib/decorator.ts: -------------------------------------------------------------------------------- 1 | import { type WebsocketServerValue, type HttpServerValue, type ServerValue } from '../../types/server' 2 | import 'reflect-metadata' 3 | 4 | export function VaasServer (vaasServer: ServerValue = { type: 'http' }) { 5 | return function (target: any, propertyKey: string, _descriptor: PropertyDescriptor) { 6 | Reflect.defineMetadata(propertyKey, vaasServer, target) 7 | } 8 | } 9 | 10 | export function Http (httpServerValue: HttpServerValue = {}) { 11 | return VaasServer({ type: 'http', ...httpServerValue }) 12 | } 13 | 14 | export function Rpc () { 15 | return VaasServer({ type: 'rpc' }) 16 | } 17 | 18 | export function Websocket (websocketServerValue: WebsocketServerValue = {}) { 19 | return VaasServer({ type: 'websocket', ...websocketServerValue }) 20 | } 21 | 22 | export function getVaasServerMap (target: any) { 23 | const vaasServerMap = new Map() 24 | const propertyKeyList = Reflect.getMetadataKeys(target) 25 | for (const propertyKey of propertyKeyList) { 26 | vaasServerMap.set(propertyKey, Reflect.getMetadata(propertyKey, target)) 27 | } 28 | return vaasServerMap 29 | } 30 | -------------------------------------------------------------------------------- /src/server/lib/error.ts: -------------------------------------------------------------------------------- 1 | import { type ErrorConfig } from '../../types/server' 2 | 3 | export function convertError2ErrorConfig ({ error }: { error: Error }) { 4 | return { 5 | message: error.message, 6 | stack: error.stack, 7 | ...error 8 | } 9 | } 10 | 11 | export function convertErrorConfig2Error ({ errorConfig }: { errorConfig: ErrorConfig }): Error { 12 | const error = new Error(errorConfig.message) 13 | return Object.assign(error, errorConfig) 14 | } 15 | -------------------------------------------------------------------------------- /src/server/lib/httpbase.ts: -------------------------------------------------------------------------------- 1 | export class HttpBase { 2 | static mergeHttpObject(target: T, source: S, mergeKeys: string[]): T { 3 | for (const mergeKey of mergeKeys) { 4 | if (source[mergeKey]) { 5 | target[mergeKey] = source[mergeKey] 6 | } 7 | } 8 | return target 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/server/lib/request.ts: -------------------------------------------------------------------------------- 1 | import type * as Koa from 'koa' 2 | import { type RequestConfig } from '../../types/server' 3 | import { HttpBase } from './httpbase' 4 | 5 | export class Request extends HttpBase { 6 | static getRequestConfigByRequest (request: Koa.Request): RequestConfig { 7 | return { 8 | charset: request.charset, 9 | length: request.length, 10 | type: request.type, 11 | headers: request.headers, 12 | body: request.body, 13 | rawBody: request.rawBody, 14 | url: request.url, 15 | origin: request.origin, 16 | href: request.href, 17 | method: request.method, 18 | path: request.path, 19 | params: {}, 20 | query: request.query, 21 | querystring: request.querystring, 22 | search: request.search, 23 | host: request.host, 24 | hostname: request.hostname, 25 | fresh: request.fresh, 26 | stale: request.stale, 27 | idempotent: request.idempotent, 28 | protocol: request.protocol, 29 | secure: request.secure, 30 | ip: request.ip, 31 | ips: request.ips 32 | } 33 | } 34 | 35 | static mergeRequestConfig2Request ({ 36 | request, requestConfig 37 | }: { request: Koa.Request, requestConfig: RequestConfig }): Koa.Request { 38 | return HttpBase.mergeHttpObject( 39 | request, requestConfig, 40 | ['url', 'method', 'path', 'query', 'querystring', 'search'] 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/server/lib/response.ts: -------------------------------------------------------------------------------- 1 | import type * as Koa from 'koa' 2 | import { type ResponseConfig } from '../../types/server' 3 | import { HttpBase } from './httpbase' 4 | 5 | export class Response extends HttpBase { 6 | static getResponseConfigByResponse (response: Koa.Response): ResponseConfig { 7 | return { 8 | headers: {}, 9 | status: undefined, 10 | message: undefined, 11 | length: undefined, 12 | type: undefined, 13 | lastModified: undefined, 14 | etag: undefined 15 | } 16 | } 17 | 18 | static mergeResponseConfig2Response ({ 19 | response, responseConfig 20 | }: { response: Koa.Response, responseConfig: ResponseConfig }): Koa.Response { 21 | response = HttpBase.mergeHttpObject( 22 | response, responseConfig, 23 | ['status', 'length', 'type', 'lastModified', 'etag'] 24 | ) 25 | if (responseConfig.headers && Object.keys(responseConfig.headers).length > 0) { 26 | // @ts-expect-error 27 | response.set(responseConfig.headers) 28 | } 29 | return response 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/server/lib/rpc.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | import { parentPort } from 'worker_threads' 3 | import { convertError2ErrorConfig } from './error' 4 | import { type ErrorMessage, type ResultMessage, type WorkerMessage, WorkerMessageType } from '../../types/server' 5 | 6 | export function workerPostMessage ( 7 | value: WorkerMessage 8 | ) { 9 | if (value.type === 'error' && value.data?.error?.message) { 10 | value.data.error = convertError2ErrorConfig({ 11 | error: value.data.error 12 | }) 13 | } 14 | try { 15 | parentPort.postMessage(value) 16 | } catch (error) { 17 | const errorMessage: ErrorMessage = { 18 | type: WorkerMessageType.error, 19 | data: { 20 | type: value.type !== WorkerMessageType.init ? value.data.type : 'http', 21 | error: convertError2ErrorConfig({ 22 | error 23 | }) 24 | } 25 | } 26 | parentPort.postMessage(errorMessage) 27 | } 28 | } 29 | 30 | export function getRpcEventName (eventName: string): string { 31 | return `rpc-${eventName}` 32 | } 33 | 34 | export const rpcEventMap = new Map void>() 35 | 36 | export async function rpcInvote (appServerName: string, params: P, context?: C): Promise { 37 | const appServerNameData = /^(\w+)\.(\w+)$/.exec(appServerName) 38 | if (!appServerNameData) { 39 | throw new Error('rpc调用必须按照app.function名方式填写,app和function名称只支持数字字母下划线') 40 | } 41 | const appName = appServerNameData[1] 42 | const serveName = appServerNameData[2] 43 | const executeId = uuidv4() 44 | workerPostMessage({ 45 | type: WorkerMessageType.execute, 46 | data: { 47 | appName, 48 | serveName, 49 | executeId, 50 | type: 'rpc', 51 | params: { params, context: context || {} } 52 | } 53 | }) 54 | return await new Promise((resolve, reject) => { 55 | rpcEventMap.set(getRpcEventName(executeId), (message: ResultMessage | ErrorMessage) => { 56 | if (message.type === 'result') { 57 | resolve(message.data.result.data); return 58 | } 59 | if (message.type === 'error') { 60 | reject(message.data.error) 61 | } 62 | }) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /src/server/lib/util.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsPromises } from 'fs' 2 | import * as path from 'path' 3 | import * as os from 'os' 4 | 5 | export const getAppEntryPath = async ({ appName, appDirPath })=>{ 6 | let appEntryPath = path.join(appDirPath, 'index.js') 7 | const appEntryPackageJsonPath = path.join(appDirPath, 'package.json') 8 | const appEntryPackageJsonStat = await fsPromises.stat(appEntryPackageJsonPath) 9 | if (appEntryPackageJsonStat.isFile()) { 10 | let appEntryPackageJson: NodeJS.Dict = {} 11 | try { 12 | appEntryPackageJson = JSON.parse((await fsPromises.readFile(appEntryPackageJsonPath)).toString()) 13 | } catch (err) { 14 | err.stack = `该微服务(${appName})的package.json文件异常,请检查(${appEntryPackageJsonPath}) \n ` + err.stack 15 | throw err 16 | } 17 | if (appEntryPackageJson.main && typeof appEntryPackageJson.main === 'string') { 18 | appEntryPath = path.join(appDirPath, appEntryPackageJson.main) 19 | } 20 | } 21 | const FileNotExistError = new Error(`该微服务(${appName})不存在入口文件(${appEntryPath})`) 22 | try { 23 | const appEntryStat = await fsPromises.stat(appEntryPath) 24 | if (!appEntryStat.isFile()) { throw FileNotExistError } 25 | } catch (err) { 26 | throw FileNotExistError 27 | } 28 | return appEntryPath 29 | } 30 | 31 | export const serverEOL = async ({ port }:{ port:number })=>{ 32 | return process.stdout.write(`${os.EOL} vaas server run: http://127.0.0.1:${port} ${os.EOL}`) 33 | } -------------------------------------------------------------------------------- /src/server/middleware/outputCatch.ts: -------------------------------------------------------------------------------- 1 | export function outputCatch ({ showErrorStack }: { showErrorStack: boolean }) { 2 | return async function (ctx, next) { 3 | try { 4 | await next() 5 | ctx.status = ctx?.response?.status || 200 6 | } catch (err) { 7 | ctx.status = err?.status || ctx?.response?.status || 500 8 | const outputData = { ...err } 9 | if(err instanceof Error) { 10 | outputData.message = err.message 11 | } 12 | if (showErrorStack) { 13 | outputData.stack = err.stack 14 | } 15 | return ctx.body = outputData 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/server/middleware/start.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | import { type Server as HttpServer, ServerResponse } from 'http' 3 | import type * as Koa from 'koa' 4 | import { WebSocketServer } from 'ws' 5 | 6 | import { type VaasWorkPool } from '../worker/pool' 7 | import { type GetAppNameByRequest, type GetByPassFlowVersion } from '../../types/server' 8 | import { Request } from '../lib/request' 9 | import { Response } from '../lib/response' 10 | import { type VaasWorkerStream } from '../worker/workerStream' 11 | 12 | async function doRequestHandler ({ 13 | ctx, 14 | getAppNameByRequest, 15 | getByPassFlowVersion 16 | }: { 17 | ctx: Koa.Context 18 | getAppNameByRequest: GetAppNameByRequest 19 | getByPassFlowVersion: GetByPassFlowVersion 20 | }) { 21 | let { appName, prefix } = await getAppNameByRequest(ctx.request) 22 | // 如果未指定App则使用默认path方法指定App 23 | if (!appName) { 24 | appName = ctx.path.split('/')[1] 25 | prefix = `/${appName}` 26 | } 27 | const { version } = await getByPassFlowVersion(appName) 28 | return { appName, prefix, version } 29 | } 30 | 31 | async function getServerWorker ({ 32 | appName, 33 | version, 34 | vaasWorkPool 35 | }: { 36 | appName: string 37 | version: string 38 | vaasWorkPool: VaasWorkPool 39 | }) { 40 | const vaasWorker = await vaasWorkPool.getWokerByAppName({ 41 | appName, 42 | version 43 | }) 44 | return vaasWorker 45 | } 46 | 47 | export function webSocketStart ({ 48 | app, 49 | server, 50 | vaasWorkPool, 51 | getAppNameByRequest, 52 | getByPassFlowVersion 53 | }: { 54 | app: Koa 55 | server: HttpServer 56 | vaasWorkPool: VaasWorkPool 57 | getAppNameByRequest: GetAppNameByRequest 58 | getByPassFlowVersion: GetByPassFlowVersion 59 | }) { 60 | const wss = new WebSocketServer({ noServer: true }) 61 | server.on('upgrade', async (request, socket, head) => { 62 | const ctx = app.createContext(request, new ServerResponse(request)) 63 | try { 64 | const { appName, prefix, version } = await doRequestHandler({ ctx, getAppNameByRequest, getByPassFlowVersion }) 65 | const vaasWorker = await getServerWorker({ 66 | appName, 67 | version, 68 | vaasWorkPool 69 | }) 70 | ctx.requestConfig = Request.getRequestConfigByRequest(ctx.request) 71 | wss.handleUpgrade(request, socket, head, (ws) => { 72 | async function webSocketMessage (wsRequestData, isBin) { 73 | let res: any 74 | let isResStream: boolean 75 | let resStream: VaasWorkerStream 76 | try { 77 | const { data, isStream, stream } = await vaasWorker.execute({ 78 | appName, 79 | executeId: uuidv4(), 80 | type: 'websocket', 81 | params: { 82 | prefix, 83 | req: ctx.requestConfig, 84 | data: isBin ? wsRequestData : wsRequestData.toString() 85 | } 86 | }) 87 | res = data 88 | isResStream = isStream 89 | resStream = stream 90 | } catch (error) { 91 | res = { 92 | message: error.message, 93 | stack: error.stack 94 | } 95 | } 96 | if (isResStream) { 97 | resStream.addWriteCallBack((chunk) => { 98 | ws.send(chunk) 99 | }) 100 | await resStream.waitWriteComplete() 101 | } else { 102 | if (res instanceof Uint8Array || typeof res === 'string') { 103 | ws.send(res) 104 | } else { 105 | ws.send(JSON.stringify(res)) 106 | } 107 | } 108 | } 109 | ws.on('message', webSocketMessage) 110 | ws.once('close', () => { 111 | ws.removeListener('message', webSocketMessage) 112 | }) 113 | }) 114 | } catch (error) { 115 | socket.destroy() 116 | throw error 117 | } 118 | }) 119 | } 120 | 121 | export function httpStart ({ 122 | vaasWorkPool, 123 | getAppNameByRequest, 124 | getByPassFlowVersion 125 | }: { 126 | vaasWorkPool: VaasWorkPool 127 | getAppNameByRequest: GetAppNameByRequest 128 | getByPassFlowVersion: GetByPassFlowVersion 129 | }) { 130 | return async function (ctx: Koa.Context) { 131 | const { appName, prefix, version } = await doRequestHandler({ ctx, getAppNameByRequest, getByPassFlowVersion }) 132 | const vaasWorker = await getServerWorker({ 133 | appName, 134 | version, 135 | vaasWorkPool 136 | }) 137 | ctx.requestConfig = Request.getRequestConfigByRequest(ctx.request) 138 | ctx.responseConfig = Response.getResponseConfigByResponse(ctx.response) 139 | const { outRequestConfig, outResponseConfig, data, isStream, stream } = await vaasWorker.execute({ 140 | appName, 141 | executeId: uuidv4(), 142 | type: 'http', 143 | params: { 144 | prefix, 145 | req: ctx.requestConfig, 146 | res: ctx.responseConfig 147 | } 148 | 149 | }) 150 | ctx.requestConfig = outRequestConfig 151 | ctx.responseConfig = outResponseConfig 152 | Request.mergeRequestConfig2Request({ request: ctx.request, requestConfig: outRequestConfig }) 153 | Response.mergeResponseConfig2Response({ response: ctx.response, responseConfig: outResponseConfig }) 154 | 155 | if (isStream) { 156 | stream.addWriteCallBack((chunk) => { 157 | ctx.res.write(chunk) 158 | }) 159 | await stream.waitWriteComplete() 160 | ctx.res.end() 161 | } else { 162 | return ctx.body = data 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/server/mini/app.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsPromises } from 'fs' 2 | import * as path from 'path' 3 | 4 | import { getAppEntryPath } from '../lib/util' 5 | export const loadApp = async ({ appsDir }: { appsDir: string }) => { 6 | const appsDirList = await fsPromises.readdir(appsDir) 7 | const appList: Array<{ appName: string, appInstance: any }> = [] 8 | for (const appName of appsDirList) { 9 | if (['.', '..'].includes(appName)) { continue } 10 | const appDirPath = path.join(appsDir, appName) 11 | const appEntryPath = await getAppEntryPath({ appName, appDirPath }) 12 | const appClass = require(appEntryPath).default 13 | const appInstance = new appClass() 14 | appList.push({ appName, appInstance }) 15 | } 16 | return appList 17 | } 18 | -------------------------------------------------------------------------------- /src/server/mini/index.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa' 2 | import { type Server as HttpServer } from 'http' 3 | import * as KoaBodyparser from 'koa-bodyparser' 4 | 5 | import { outputCatch } from '../middleware/outputCatch' 6 | import { serverEOL } from '../lib/util' 7 | import { loadHttpRouter } from './router' 8 | import { loadApp } from './app' 9 | import { loadWebsocket } from './websocket' 10 | 11 | /** 12 | * miniVaasServer只有单进程单线程 13 | * 并且无VM直接运行在宿主环境中 14 | * 去除限制和配置获取,同时增加中间件的支持 15 | * 可以仅作为框架使用 16 | */ 17 | export class MiniVaasServer { 18 | server: HttpServer 19 | async run ({ 20 | appsDir, port, 21 | showErrorStack, 22 | prefix, 23 | middlewares 24 | }: { 25 | appsDir: string, port: number 26 | showErrorStack: boolean 27 | prefix: string 28 | middlewares: Koa.Middleware[] 29 | }): Promise { 30 | const app = new Koa() 31 | app.use(outputCatch({ showErrorStack })) 32 | app.use(KoaBodyparser({ 33 | formLimit: '30mb', 34 | jsonLimit: '30mb', 35 | textLimit: '30mb', 36 | xmlLimit: '30mb' 37 | })) 38 | for (const middleware of middlewares) { 39 | app.use(middleware) 40 | } 41 | const appList = await loadApp({ appsDir }) 42 | await loadHttpRouter({ app, prefix, appList }) 43 | return await new Promise((resolve) => { 44 | this.server = app.listen(port, () => { 45 | loadWebsocket({ app, prefix, appList, server: this.server }) 46 | serverEOL({ port }) 47 | resolve(app) 48 | }) 49 | }) 50 | } 51 | 52 | async close (): Promise { 53 | return await new Promise((resolve, reject) => { 54 | this.server.close((error) => { 55 | if (error)reject(error) 56 | resolve(true) 57 | }) 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/server/mini/router.ts: -------------------------------------------------------------------------------- 1 | import * as Router from 'koa-router' 2 | import type * as Koa from 'koa' 3 | 4 | import { getVaasServerMap } from '../lib/decorator' 5 | 6 | const loadRouter = async ({ prefix, appList, routerFunction }: { prefix: string, routerFunction: ({ serveConfig, appRouter, appName, serveName, appInstance }) => void, appList: Array<{ appName: string, appInstance: any }> }) => { 7 | const router = prefix ? new Router({ prefix }) : new Router() 8 | for (const appInfo of appList) { 9 | const { appName, appInstance } = appInfo 10 | const vaasServerMap = getVaasServerMap(appInstance) 11 | const appRouter = new Router() 12 | for (const [serveName, serveConfig] of vaasServerMap) { 13 | if (['http', 'websocket'].includes(serveConfig.type)) { 14 | routerFunction({ serveConfig, appRouter, appName, serveName, appInstance }) 15 | } else { 16 | throw new Error(`[${appName}][${serveName}]使用的是简易框架,不支持${serveConfig.type}类型`) 17 | } 18 | } 19 | router.use(`/${appName}`, appRouter.routes(), appRouter.allowedMethods()) 20 | } 21 | return router 22 | } 23 | 24 | export const loadHttpRouter = async ({ app, prefix, appList }: { app: Koa, prefix: string, appList: Array<{ appName: string, appInstance: any }> }) => { 25 | const router = await loadRouter({ 26 | prefix, 27 | appList, 28 | routerFunction: ({ serveConfig, appRouter, serveName, appInstance }) => { 29 | if (serveConfig.type === 'http') { 30 | const method = serveConfig.method ? serveConfig.method : 'all' 31 | const routerName = serveConfig.routerName ? serveConfig.routerName : `/${serveName}` 32 | appRouter[method](routerName, async (ctx) => { 33 | ctx.body = await appInstance[serveName]({ req: ctx.request, res: ctx.response }) 34 | }) 35 | } 36 | } 37 | }) 38 | const routes = router.routes() 39 | app.use(routes).use(router.allowedMethods()) 40 | } 41 | 42 | export const loadWebsocketRouter = async ({ prefix, appList }: { prefix: string, appList: Array<{ appName: string, appInstance: any }> }) => { 43 | const router = await loadRouter({ 44 | prefix, 45 | appList, 46 | routerFunction: ({ serveConfig, appRouter, serveName, appInstance }) => { 47 | if (serveConfig.type === 'websocket') { 48 | const method = serveConfig.method ? serveConfig.method : 'all' 49 | const routerName = serveConfig.routerName ? serveConfig.routerName : `/${serveName}` 50 | appRouter[method](routerName, async (ctx) => { 51 | const data = await appInstance[serveName]({ req: ctx.request, data: ctx._websocketData }) 52 | ctx._websocketSend(data) 53 | }) 54 | } 55 | } 56 | }) 57 | const routes = router.routes() 58 | return routes 59 | } 60 | -------------------------------------------------------------------------------- /src/server/mini/websocket.ts: -------------------------------------------------------------------------------- 1 | import type * as Koa from 'koa' 2 | import { WebSocketServer } from 'ws' 3 | import { type Server as HttpServer, ServerResponse } from 'http' 4 | import { loadWebsocketRouter } from './router' 5 | export const loadWebsocket = async ({ app, prefix, appList, server }: { app: Koa, prefix: string, appList: Array<{ appName: string, appInstance: any }>, server: HttpServer }) => { 6 | const wss = new WebSocketServer({ noServer: true }) 7 | const routes = await loadWebsocketRouter({ prefix, appList }) 8 | server.on('upgrade', async (request, socket, head) => { 9 | const ctx = app.createContext(request, new ServerResponse(request)) 10 | // 这里使用中间件的方式调用 11 | wss.handleUpgrade(request, socket, head, (ws) => { 12 | const send = (data) => { 13 | if (typeof data !== 'string' && !(data instanceof Buffer)) { 14 | data = JSON.stringify(data) 15 | } 16 | return ws.send(data) 17 | } 18 | const message = (data) => { 19 | ctx._websocketData = data 20 | ctx._websocketSend = send 21 | // @ts-expect-error 22 | routes(ctx) 23 | } 24 | ws.on('message', message) 25 | const clean = () => { 26 | ws.removeListener('message', message) 27 | ws.removeListener('error', clean) 28 | ws.removeListener('close', clean) 29 | socket.destroy() 30 | request.destroy() 31 | } 32 | ws.once('close', clean) 33 | ws.once('error', clean) 34 | }) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/server/worker/pool.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fsPromises from 'fs/promises' 3 | // import { convertErrorConfig2Error } from '../lib/error' 4 | import { type GetAppConfigByAppName, type GetByPassFlowVersion, type WorkerConfig } from '../../types/server' 5 | import { VaasWorker, VaasWorkerSet } from './workerManage' 6 | interface AppPool { 7 | vaasWorkerSet: VaasWorkerSet 8 | } 9 | export class VaasWorkPool { 10 | pool: Map> = new Map>() 11 | workerRecycleCheckTime: number 12 | appsDir: string 13 | getAppConfigByAppName: GetAppConfigByAppName 14 | getByPassFlowVersion: GetByPassFlowVersion 15 | static instance: VaasWorkPool = null 16 | constructor ({ 17 | appsDir, 18 | getAppConfigByAppName, 19 | getByPassFlowVersion 20 | }: { 21 | appsDir: string 22 | getAppConfigByAppName: GetAppConfigByAppName 23 | getByPassFlowVersion: GetByPassFlowVersion 24 | }) { 25 | if (VaasWorkPool.instance) { 26 | return VaasWorkPool.instance 27 | } 28 | VaasWorkPool.instance = this 29 | this.getAppConfigByAppName = getAppConfigByAppName 30 | this.getByPassFlowVersion = getByPassFlowVersion 31 | this.appsDir = appsDir 32 | } 33 | 34 | async prepareWorker () { 35 | const appNameList = await fsPromises.readdir(this.appsDir) 36 | for (const appName of appNameList) { 37 | if (['.', '..'].includes(appName)) { continue } 38 | const { version } = await this.getByPassFlowVersion(appName) 39 | await this.getWokerByAppName({ 40 | appName, 41 | version 42 | }) 43 | } 44 | } 45 | 46 | private recycle ({ 47 | vaasWorker, vaasWorkerSet, 48 | appName, version, recycleTime 49 | }: { 50 | vaasWorker: VaasWorker 51 | vaasWorkerSet: VaasWorkerSet 52 | appName: string 53 | version: string 54 | recycleTime: number 55 | }) { 56 | const recycleTimeId = setTimeout(() => { 57 | const appPool = this.pool.get(appName) 58 | if (vaasWorker.recyclable()) { 59 | vaasWorkerSet.delete(vaasWorker) 60 | vaasWorker.terminate() 61 | vaasWorker.removeAllListeners() 62 | vaasWorker.poolInstance = null 63 | if (vaasWorkerSet.size <= 0) { 64 | appPool && appPool.delete(version) 65 | } 66 | if (appPool && appPool.size <= 0) { 67 | this.pool.delete(appName) 68 | } 69 | } else { 70 | this.recycle({ vaasWorker, vaasWorkerSet, appName, version, recycleTime }) 71 | } 72 | clearTimeout(recycleTimeId) 73 | }, recycleTime + 1) 74 | } 75 | 76 | private getWorker ({ 77 | appsDir, appName, version, 78 | allowModuleSet, recycleTime, resourceLimits, 79 | useVmLoadDependencies 80 | }: WorkerConfig): VaasWorker { 81 | const appDirPath = path.join(appsDir, appName, version) 82 | const worker = new VaasWorker(path.join(__dirname, 'worker.js'), { 83 | appName, 84 | version, 85 | poolInstance: this, 86 | resourceLimits, 87 | recycleTime, 88 | workerData: { appsDir, appName, appDirPath, allowModuleSet, useVmLoadDependencies } 89 | }) 90 | return worker 91 | } 92 | 93 | async getWorkConfigByAppName ({ appName, version }): Promise { 94 | const appConfig = await this.getAppConfigByAppName(appName) 95 | return { 96 | appName, 97 | version, 98 | appsDir: this.appsDir, 99 | maxWorkerNum: appConfig.maxWorkerNum, 100 | allowModuleSet: new Set(appConfig.allowModuleSet), 101 | recycleTime: appConfig.timeout, 102 | resourceLimits: appConfig.resourceLimits, 103 | useVmLoadDependencies: appConfig.useVmLoadDependencies 104 | } 105 | } 106 | 107 | async getWokerByAppName ({ 108 | appName, 109 | version 110 | }: { 111 | appName: string 112 | version: string 113 | }): Promise { 114 | let appPool: Map 115 | if (this.pool.has(appName)) { 116 | appPool = this.pool.get(appName) 117 | } else { 118 | appPool = new Map() 119 | this.pool.set(appName, appPool) 120 | } 121 | const workConfig = await this.getWorkConfigByAppName({ appName, version }) 122 | if (appPool.has(version)) { 123 | const { vaasWorkerSet } = appPool.get(version) 124 | if (vaasWorkerSet.size < vaasWorkerSet.maxSize) { 125 | const vaasWorker = this.getWorker(workConfig) 126 | await vaasWorker.init() 127 | // 添加work和判断work长度中间不能使用await否则非原子操作产生work击穿 128 | vaasWorkerSet.add(vaasWorker) 129 | this.recycle({ 130 | vaasWorker, 131 | vaasWorkerSet, 132 | appName, 133 | version, 134 | recycleTime: workConfig.recycleTime 135 | }) 136 | } 137 | return vaasWorkerSet.next() 138 | } 139 | const vaasWorker = this.getWorker(workConfig) 140 | await vaasWorker.init() 141 | const vaasWorkerSet = new VaasWorkerSet([vaasWorker], workConfig.maxWorkerNum) 142 | // 添加work和appPool.has判断中间不能使用await否则非原子操作产生work击穿 143 | appPool.set(version, { vaasWorkerSet }) 144 | this.recycle({ 145 | vaasWorker, 146 | vaasWorkerSet, 147 | appName, 148 | version, 149 | recycleTime: workConfig.recycleTime 150 | }) 151 | return appPool.get(version).vaasWorkerSet.next() 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/server/worker/worker.ts: -------------------------------------------------------------------------------- 1 | import { dynamicRun, proxyData } from 'vaas-core' 2 | import { parentPort, workerData } from 'worker_threads' 3 | import { Readable, Writable, pipeline } from 'stream' 4 | import { match as pathMatch } from 'path-to-regexp' 5 | 6 | import { getAppEntryPath } from '../lib/util' 7 | import { getVaasServerMap } from '../lib/decorator' 8 | import { workerPostMessage, rpcEventMap, getRpcEventName } from '../lib/rpc' 9 | import { 10 | type WorkerMessage, type ExecuteMessageBody, type ServerType, 11 | type ServerValue, type ServerRouterValue, WorkerMessageType 12 | } from '../../types/server' 13 | import { deprecate } from 'util' 14 | 15 | const packageInfo = require('../../../package.json') 16 | 17 | const pipelinePromise = async (source: any, destination: NodeJS.WritableStream) => { 18 | return await new Promise((resolve, reject) => { 19 | const writableStream = pipeline(source, destination, (error) => { 20 | if (error) { reject(error); return } 21 | resolve(writableStream) 22 | }) 23 | }) 24 | } 25 | 26 | const getWorkerRouteMap = (prefix, appConfig: Map) => { 27 | let routePrefix = prefix 28 | if (!prefix || prefix === '/') { 29 | routePrefix = '' 30 | } 31 | if (getWorkerRouteMap.appRouteMap.has(routePrefix)) { 32 | return getWorkerRouteMap.appRouteMap.get(routePrefix) 33 | } 34 | const workerRouteMap = new Map() 35 | for (const [serveName, serveConfig] of appConfig) { 36 | workerRouteMap.set(serveName, { 37 | type: serveConfig.type, 38 | method: serveConfig.method, 39 | routerName: serveConfig.routerName, 40 | routerFn: pathMatch(`${routePrefix}${serveConfig.routerName || `/${serveName}`}`) 41 | }) 42 | } 43 | getWorkerRouteMap.appRouteMap.set(routePrefix, workerRouteMap) 44 | return workerRouteMap 45 | } 46 | getWorkerRouteMap.appRouteMap = new Map>() 47 | 48 | const workerRouteMatch = ({ path, method }, workerRouteMap: Map) => { 49 | const lowerCaseMethod = method.toLowerCase() 50 | for (const [serveName, serveConfig] of workerRouteMap) { 51 | if (serveConfig.method && serveConfig.method !== lowerCaseMethod) { 52 | continue 53 | } 54 | const matchResult = serveConfig.routerFn(path) 55 | if (matchResult) { 56 | return { 57 | serveName, 58 | params: matchResult.params 59 | } 60 | } 61 | } 62 | return null 63 | } 64 | 65 | let lastExecuteType: ServerType = null 66 | export class VaasWorker { 67 | postExecuteMessage ({ executeMessage, data, isComplete, isStream }: { executeMessage: ExecuteMessageBody, data: any, isComplete: boolean, isStream: boolean }) { 68 | if (executeMessage.type === 'http') { 69 | workerPostMessage( 70 | { 71 | type: WorkerMessageType.result, 72 | data: { 73 | result: { 74 | isComplete, 75 | isStream, 76 | outRequestConfig: executeMessage.params.req, 77 | outResponseConfig: executeMessage.params.res, 78 | data 79 | }, 80 | type: executeMessage.type, 81 | executeId: executeMessage.executeId 82 | } 83 | } 84 | ) 85 | } else { 86 | workerPostMessage( 87 | { 88 | type: WorkerMessageType.result, 89 | data: { 90 | result: { 91 | isComplete, 92 | isStream, 93 | data 94 | }, 95 | type: executeMessage.type, 96 | executeId: executeMessage.executeId 97 | } 98 | } 99 | ) 100 | } 101 | } 102 | 103 | async run () { 104 | const appClass = await this.loadServer() 105 | const app = new appClass() 106 | const appConfig = getVaasServerMap(app) 107 | parentPort.on('message', async (message: WorkerMessage) => { 108 | if (message.type === 'result' || message.type === 'error') { 109 | const callback = rpcEventMap.get(getRpcEventName(message.data.executeId)) 110 | if (callback instanceof Function) { 111 | callback(message); return 112 | } 113 | } 114 | if (message.type !== 'execute') { return } 115 | const executeMessage: ExecuteMessageBody = message.data 116 | lastExecuteType = executeMessage.type 117 | if (executeMessage.type !== 'rpc') { 118 | const workerRouteMap = getWorkerRouteMap(executeMessage.params?.prefix, appConfig) 119 | const workerRouteMatchRes = workerRouteMatch({ path: executeMessage.params.req.path, method: executeMessage.params.req.method }, workerRouteMap) 120 | if (!workerRouteMatchRes) { 121 | throw new Error(`this App(${executeMessage.appName}) not path has matched (${executeMessage.params.req.method})[${executeMessage.params.req.path}]`) 122 | } 123 | executeMessage.params.req.params = workerRouteMatchRes.params 124 | executeMessage.serveName = workerRouteMatchRes.serveName 125 | } 126 | try { 127 | if (!executeMessage.serveName) { 128 | throw new Error(`this App(${executeMessage.appName}) not path has matched serveName`) 129 | } 130 | const serveConfig = appConfig.get(executeMessage.serveName) 131 | if (executeMessage.type !== serveConfig.type) { 132 | throw new Error(`appName[${executeMessage.appName}]'s serveName[${ 133 | executeMessage.serveName 134 | }] not matched type[${executeMessage.type}]`) 135 | } 136 | const data = await app[executeMessage.serveName](executeMessage.params) 137 | if (data instanceof Readable) { 138 | const ws = new Writable({ 139 | write: (chunk, encoding, callback) => { 140 | this.postExecuteMessage({ executeMessage, data: { chunk, encoding }, isComplete: false, isStream: true }) 141 | callback() 142 | } 143 | }) 144 | await pipelinePromise(data, ws) 145 | this.postExecuteMessage({ executeMessage, data: { chunk: null, encoding: '' }, isComplete: true, isStream: true }) 146 | } else { 147 | this.postExecuteMessage({ executeMessage, data, isComplete: true, isStream: false }) 148 | } 149 | } catch (error) { 150 | workerPostMessage( 151 | { 152 | type: WorkerMessageType.error, 153 | data: { 154 | type: executeMessage.type, 155 | error, 156 | executeId: executeMessage.executeId 157 | } 158 | } 159 | ) 160 | } 161 | }) 162 | workerPostMessage({ 163 | type: WorkerMessageType.init, 164 | data: { 165 | status: 'ok' 166 | } 167 | }) 168 | } 169 | 170 | async loadServer (): Promise { 171 | const { appName, appDirPath } = workerData 172 | // 关于文件的存在性,在初始化线程前判断,节约线程开支 173 | const appProgram = dynamicRun({ 174 | filepath: await getAppEntryPath({ appName, appDirPath }), 175 | extendVer: {}, 176 | isGlobalContext:true, 177 | overwriteRequire: (callbackData) => { 178 | const useVmLoadDependencies = workerData.useVmLoadDependencies 179 | if (!useVmLoadDependencies && callbackData.moduleId[0] !== '.' && callbackData.moduleId[0] !== '/') { 180 | return callbackData.nativeRequire(callbackData.moduleId) 181 | } 182 | if (packageInfo.name === callbackData.moduleId) { 183 | return callbackData.nativeRequire(callbackData.moduleId) 184 | } 185 | if (callbackData.modulePath[0] === '/') { 186 | // node_module和相对路径处理方法,这样引用不会丢失类型判断 187 | if (!callbackData.modulePath.includes(workerData.appDirPath)) { 188 | throw new Error(`file[${ 189 | callbackData.filepath 190 | }] can't require module[${ 191 | callbackData.modulePath 192 | }] beyond appDirPath[${ 193 | workerData.appDirPath 194 | }], use ${packageInfo.name}.rpcInvote('app.server',{...}) to call server,please`) 195 | } 196 | if (!(/\.js$/.exec(callbackData.modulePath))) { 197 | if (/\.node$/.exec(callbackData.modulePath)) { 198 | deprecate(() => {}, `c++ extension method will be deprecated! [${callbackData.modulePath}]`)() 199 | } 200 | return callbackData.nativeRequire(callbackData.modulePath) 201 | } 202 | } else { 203 | // 系统模块处理方法 204 | const allowModuleSet: Set = workerData.allowModuleSet 205 | if (allowModuleSet.has('*')) { return callbackData.nativeRequire(callbackData.modulePath) } 206 | if (allowModuleSet.has(callbackData.modulePath)) { return callbackData.nativeRequire(callbackData.modulePath) } 207 | throw new Error(`file[${ 208 | callbackData.filepath 209 | }] can't require module[${ 210 | callbackData.modulePath 211 | }] beyond appDirPath[${ 212 | workerData.appDirPath 213 | }], add module[${ 214 | callbackData.modulePath 215 | }] to allowModuleSet,please`) 216 | } 217 | } 218 | }) 219 | return appProgram.default 220 | } 221 | } 222 | 223 | new VaasWorker().run().catch((error) => { 224 | workerPostMessage( 225 | { 226 | type: WorkerMessageType.error, 227 | data: { 228 | type: lastExecuteType, 229 | error 230 | } 231 | } 232 | ) 233 | }) 234 | 235 | process.on('uncaughtException', (error) => { 236 | workerPostMessage( 237 | { 238 | type: WorkerMessageType.error, 239 | data: { 240 | type: lastExecuteType, 241 | error 242 | } 243 | } 244 | ) 245 | }) 246 | 247 | process.on('unhandledRejection', (error) => { 248 | workerPostMessage( 249 | { 250 | type: WorkerMessageType.error, 251 | data: { 252 | type: lastExecuteType, 253 | error 254 | } 255 | } 256 | ) 257 | }) 258 | -------------------------------------------------------------------------------- /src/server/worker/workerManage.ts: -------------------------------------------------------------------------------- 1 | import { Worker, type WorkerOptions } from 'worker_threads' 2 | import { Buffer } from 'buffer' 3 | import { convertError2ErrorConfig, convertErrorConfig2Error } from '../lib/error' 4 | import { 5 | type WorkerMessage, type ExecuteMessageBody, 6 | type ExecuteMessage, type ResultMessage, type ErrorMessage, 7 | WorkerMessageType 8 | } from '../../types/server' 9 | import { VaasWorkerStream } from './workerStream' 10 | interface VaasWorkerOptions extends WorkerOptions { 11 | appName: string 12 | version: string 13 | recycleTime: number 14 | poolInstance: any 15 | } 16 | 17 | export class VaasWorker extends Worker { 18 | appName: string 19 | version: string 20 | poolInstance: any 21 | createAt: number 22 | updateAt: number 23 | recycleTime: number 24 | messageStatus: 'runing' | null 25 | isExit: boolean 26 | private latestExecuteId: string 27 | private readonly messageEventMap = new Map 29 | callback: (message: WorkerMessage) => void 30 | }>() 31 | 32 | constructor (filename: string | URL, options?: VaasWorkerOptions) { 33 | super(filename, options) 34 | this.createAt = Date.now() 35 | this.updateAt = Date.now() 36 | this.appName = options.appName 37 | this.version = options.version 38 | this.recycleTime = options.recycleTime 39 | this.poolInstance = options.poolInstance 40 | } 41 | 42 | async init () { 43 | await this.doMessage() 44 | } 45 | 46 | private getExecuteEventName (eventName: string): string { 47 | return `execute-${eventName}` 48 | } 49 | 50 | private async doMessage () { 51 | if (this.messageStatus === 'runing') { return } 52 | this.messageStatus = 'runing' 53 | return await new Promise((resolve, reject) => { 54 | const messageFunc = async (message: WorkerMessage) => { 55 | if (message.type === 'execute') { 56 | const executeMessageBody = message.data 57 | try { 58 | const vaasWorker: VaasWorker = await this.poolInstance.getWokerByAppName({ appName: executeMessageBody.appName, version: this.version }) 59 | const result = await vaasWorker.execute(executeMessageBody) 60 | const resultMessage: ResultMessage = { 61 | type: WorkerMessageType.result, 62 | data: { 63 | executeId: executeMessageBody.executeId, 64 | type: executeMessageBody.type, 65 | result 66 | } 67 | } 68 | this.postMessage(resultMessage) 69 | } catch (error) { 70 | const errorMessage: ErrorMessage = { 71 | type: WorkerMessageType.error, 72 | data: { 73 | type: executeMessageBody.type, 74 | executeId: executeMessageBody.executeId, 75 | error: convertError2ErrorConfig({ error }) 76 | } 77 | } 78 | this.postMessage(errorMessage) 79 | } 80 | } else if (message.type === WorkerMessageType.error || message.type === WorkerMessageType.result) { 81 | const { executeId } = message.data 82 | const messageEvent = this.messageEventMap.get(this.getExecuteEventName(executeId || this.latestExecuteId)) 83 | if (messageEvent?.callback instanceof Function) { 84 | messageEvent.callback(message) 85 | } 86 | if (message.type === WorkerMessageType.error) { 87 | reject(convertErrorConfig2Error({ errorConfig: message.data.error })) 88 | } 89 | } else if (message.type === WorkerMessageType.init) { 90 | resolve(true) 91 | } 92 | } 93 | this.on('message', messageFunc) 94 | }) 95 | } 96 | 97 | async execute ({ appName, serveName, executeId, type, params }: ExecuteMessageBody): Promise { 98 | if (this.isExit) { 99 | if (this.latestExecuteId) { 100 | const messageEvent = this.messageEventMap.get(this.getExecuteEventName(this.latestExecuteId)) 101 | throw new Error(`appName[${ 102 | appName 103 | }] worker was exit!maybe cause by ${ 104 | messageEvent?.info ? JSON.stringify(messageEvent.info) : 'unkown' 105 | } request`) 106 | } 107 | throw new Error(`appName[${appName}] worker was exit`) 108 | } 109 | this.updateAt = Date.now() 110 | const executeMessage: ExecuteMessage = { 111 | type: WorkerMessageType.execute, 112 | data: { 113 | type, 114 | appName, 115 | serveName, 116 | executeId, 117 | params 118 | } 119 | } 120 | this.latestExecuteId = executeId 121 | this.postMessage(executeMessage) 122 | return await new Promise((resolve, reject) => { 123 | let isComplete = false 124 | const messageEventName = this.getExecuteEventName(executeId) 125 | const clearMessage = () => { 126 | // clearTimeout(timeoutId) //没必要清除 127 | this.messageEventMap.delete(messageEventName) 128 | if (!isComplete) { 129 | reject(new Error(`worker run time out[${this.recycleTime}]`)) 130 | } 131 | } 132 | const timeoutId = setTimeout(clearMessage, this.recycleTime) 133 | const workerStream = new VaasWorkerStream() 134 | this.messageEventMap.set(messageEventName, { 135 | // 不建议info过大,对性能造成影响 136 | info: { 137 | type, 138 | appName, 139 | serveName, 140 | executeId 141 | }, 142 | callback: (message: WorkerMessage) => { 143 | isComplete = true 144 | if (message.type === 'result') { 145 | isComplete = message.data.result.isComplete 146 | } 147 | if (isComplete) { 148 | clearMessage() 149 | // 这里是为了性能优化,防止无效setTimeout积压 150 | clearTimeout(timeoutId) 151 | } 152 | if (message.type === 'result') { 153 | // 兼容低版本node的buffer未转化问题 154 | if (message.data.result.data instanceof Uint8Array) { 155 | message.data.result.data = Buffer.from(message.data.result.data) 156 | } 157 | 158 | if (message.data.result.isStream) { 159 | if (message.data.result.data.chunk instanceof Uint8Array) { 160 | message.data.result.data.chunk = Buffer.from(message.data.result.data.chunk) 161 | } 162 | if (isComplete) { 163 | workerStream.writeComplete() 164 | } else { 165 | workerStream.write(message.data.result.data.chunk) 166 | } 167 | message.data.result.stream = workerStream 168 | } 169 | resolve(message.data.result); return 170 | } 171 | if (message.type === 'error') { 172 | reject(convertErrorConfig2Error({ errorConfig: message.data.error })) 173 | } 174 | } 175 | }) 176 | }) 177 | } 178 | 179 | recyclable () { 180 | return this.isExit || (this.updateAt + this.recycleTime < Date.now()) 181 | } 182 | } 183 | 184 | export class VaasWorkerSet extends Set { 185 | private workerIterator: IterableIterator 186 | maxSize: number = 0 187 | constructor (iterable: Iterable, maxSize: number) { 188 | super(iterable) 189 | this.maxSize = maxSize 190 | } 191 | 192 | next () { 193 | if (!this.workerIterator) { this.workerIterator = this.values() } 194 | const nextValue = this.workerIterator.next() 195 | if (nextValue.done) { 196 | this.workerIterator = this.values() 197 | return this.workerIterator.next().value 198 | } 199 | return nextValue.value 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/server/worker/workerStream.ts: -------------------------------------------------------------------------------- 1 | export class VaasWorkerStream { 2 | private callback: (chunk: Buffer) => void 3 | private completeCallback: () => void 4 | write (chunk: Buffer) { 5 | // 防止未添加callback就运行 6 | setImmediate(() => { 7 | this.callback(chunk) 8 | }) 9 | } 10 | 11 | writeComplete () { 12 | // 防止completeCallback快于callback 13 | setImmediate(() => { 14 | this.completeCallback() 15 | }) 16 | } 17 | 18 | addWriteCallBack (callback: (chunk: Buffer) => void) { 19 | this.callback = callback 20 | } 21 | 22 | async waitWriteComplete () { 23 | return await new Promise((resolve) => { 24 | this.completeCallback = () => { 25 | resolve(true) 26 | } 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/server.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | // import * as path from 'path'; 3 | import { describe, it } from 'mocha' 4 | // import {VaasServer} from '../server/index'; 5 | 6 | // const server = new VaasServer() 7 | 8 | // server.run({ 9 | // appsDir:path.join(__dirname,'apps'), 10 | // port:8080, 11 | // getAppNameByRequest:(request)=>{ 12 | // return '' 13 | // }, 14 | // getAppConfigByAppName:(appName)=>{ 15 | // return { 16 | // maxWorkerNum:2, 17 | // timeout:3000 18 | // } 19 | // }, 20 | // showErrorStack:true 21 | // }) 22 | 23 | describe('Array', function () { 24 | describe('#indexOf()', function () { 25 | it('should return -1 when the value is not present', function () { 26 | assert.equal([1, 2, 3].indexOf(4), -1) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/types/server.ts: -------------------------------------------------------------------------------- 1 | import { type OutgoingHttpHeader } from 'http' 2 | import { type ResourceLimits } from 'worker_threads' 3 | import type * as Koa from 'koa' 4 | import { type VaasWorkerStream } from '../server/worker/workerStream' 5 | 6 | export type ServerType = 'http' | 'websocket' | 'rpc' 7 | 8 | export interface ServerValue { 9 | type: ServerType 10 | method?: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' 11 | routerName?: string 12 | } 13 | 14 | export interface HttpServerValue { 15 | method?: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' 16 | routerName?: string 17 | } 18 | 19 | // 目前Websocket和HTTP基本一致 20 | export type WebsocketServerValue = HttpServerValue 21 | 22 | export interface ServerRouterValue extends ServerValue { 23 | routerFn?: Function 24 | } 25 | 26 | export interface AppConfig { 27 | maxWorkerNum: number 28 | allowModuleSet: Set 29 | timeout: number 30 | resourceLimits?: ResourceLimits 31 | useVmLoadDependencies: boolean 32 | } 33 | 34 | export type GetAppNameByRequest = (request: Koa.Request) => Promise<{ appName: string, prefix: string }> 35 | 36 | export type GetAppConfigByAppName = (appName: string) => Promise 37 | 38 | export type GetByPassFlowVersion = (appName: string) => Promise<{ version: string }> 39 | 40 | export interface VaasConfig { 41 | appsDir: string 42 | port: number 43 | getAppNameByRequest: GetAppNameByRequest 44 | getAppConfigByAppName: GetAppConfigByAppName 45 | getByPassFlowVersion: GetByPassFlowVersion 46 | showErrorStack: boolean 47 | isPrepareWorker: boolean 48 | } 49 | 50 | export interface RequestConfig { 51 | /** 52 | * Get the charset when present or undefined. 53 | */ 54 | readonly charset: string 55 | 56 | /** 57 | * Return parsed Content-Length when present. 58 | */ 59 | readonly length: number 60 | 61 | /** 62 | * Return the request mime type void of 63 | * parameters such as "charset". 64 | */ 65 | readonly type: string 66 | 67 | /** 68 | * Return request header, alias as request.header 69 | */ 70 | readonly headers: NodeJS.Dict 71 | 72 | /** 73 | * Get request body. 74 | */ 75 | readonly body?: Record 76 | 77 | /** 78 | * Get query string. 79 | */ 80 | readonly rawBody: string 81 | 82 | /** 83 | * Get/Set request URL. 84 | */ 85 | url: string 86 | 87 | /** 88 | * Get origin of URL. 89 | */ 90 | readonly origin: string 91 | 92 | /** 93 | * Get full request URL. 94 | */ 95 | readonly href: string 96 | 97 | /** 98 | * Get/Set request method. 99 | */ 100 | method: string 101 | 102 | /** 103 | * Get request pathname. 104 | * Set pathname, retaining the query-string when present. 105 | */ 106 | path: string 107 | 108 | /** 109 | * Get parsed routerName-params. 110 | * Set routerName-params as an object. 111 | */ 112 | params: NodeJS.Dict 113 | 114 | /** 115 | * Get parsed query-string. 116 | * Set query-string as an object. 117 | */ 118 | query: NodeJS.Dict 119 | 120 | /** 121 | * Get/Set query string. 122 | */ 123 | querystring: string 124 | 125 | /** 126 | * Get the search string. Same as the querystring 127 | * except it includes the leading ?. 128 | * 129 | * Set the search string. Same as 130 | * response.querystring= but included for ubiquity. 131 | */ 132 | search: string 133 | 134 | /** 135 | * Parse the "Host" header field host 136 | * and support X-Forwarded-Host when a 137 | * proxy is enabled. 138 | */ 139 | readonly host: string 140 | 141 | /** 142 | * Parse the "Host" header field hostname 143 | * and support X-Forwarded-Host when a 144 | * proxy is enabled. 145 | */ 146 | readonly hostname: string 147 | 148 | /** 149 | * Check if the request is fresh, aka 150 | * Last-Modified and/or the ETag 151 | * still match. 152 | */ 153 | readonly fresh: boolean 154 | 155 | /** 156 | * Check if the request is stale, aka 157 | * "Last-Modified" and / or the "ETag" for the 158 | * resource has changed. 159 | */ 160 | readonly stale: boolean 161 | 162 | /** 163 | * Check if the request is idempotent. 164 | */ 165 | readonly idempotent: boolean 166 | 167 | /** 168 | * Return the protocol string "http" or "https" 169 | * when requested with TLS. When the proxy setting 170 | * is enabled the "X-Forwarded-Proto" header 171 | * field will be trusted. If you're running behind 172 | * a reverse proxy that supplies https for you this 173 | * may be enabled. 174 | */ 175 | readonly protocol: string 176 | 177 | /** 178 | * Short-hand for: 179 | * 180 | * this.protocol == 'https' 181 | */ 182 | readonly secure: boolean 183 | 184 | /** 185 | * Request remote address. Supports X-Forwarded-For when app.proxy is true. 186 | */ 187 | readonly ip: string 188 | 189 | /** 190 | * When `app.proxy` is `true`, parse 191 | * the "X-Forwarded-For" ip address list. 192 | * 193 | * For example if the value were "client, proxy1, proxy2" 194 | * you would receive the array `["client", "proxy1", "proxy2"]` 195 | * where "proxy2" is the furthest down-stream. 196 | */ 197 | readonly ips: string[] 198 | } 199 | 200 | export interface ResponseConfig { 201 | /** 202 | * Return response header. 203 | */ 204 | headers: NodeJS.Dict 205 | /** 206 | * Get/Set response status code. 207 | */ 208 | status: number 209 | 210 | /** 211 | * Get response status message 212 | */ 213 | readonly message: string 214 | 215 | /** 216 | * Return parsed response Content-Length when present. 217 | * Set Content-Length field to `n`. 218 | */ 219 | length: number 220 | 221 | /** 222 | * Return the response mime type void of 223 | * parameters such as "charset". 224 | * 225 | * Set Content-Type response header with `type` through `mime.lookup()` 226 | * when it does not contain a charset. 227 | * 228 | * Examples: 229 | * 230 | * this.type = '.html'; 231 | * this.type = 'html'; 232 | * this.type = 'json'; 233 | * this.type = 'application/json'; 234 | * this.type = 'png'; 235 | */ 236 | type: string 237 | 238 | /** 239 | * Get the Last-Modified date in Date form, if it exists. 240 | * Set the Last-Modified date using a string or a Date. 241 | * 242 | * this.response.lastModified = new Date(); 243 | * this.response.lastModified = '2013-09-13'; 244 | */ 245 | lastModified: Date 246 | 247 | /** 248 | * Get/Set the ETag of a response. 249 | * This will normalize the quotes if necessary. 250 | * 251 | * this.response.etag = 'md5hashsum'; 252 | * this.response.etag = '"md5hashsum"'; 253 | * this.response.etag = 'W/"123456789"'; 254 | * 255 | * @param {String} etag 256 | * @api public 257 | */ 258 | etag: string 259 | } 260 | 261 | export interface HttpParams { 262 | req: RequestConfig 263 | res: ResponseConfig 264 | } 265 | 266 | export interface RpcParams { 267 | params: P 268 | context: C 269 | } 270 | 271 | export interface ErrorConfig { 272 | message: string 273 | name: string 274 | stack: string 275 | } 276 | 277 | export interface WorkerConfig { 278 | appName: string 279 | version: string 280 | appsDir: string 281 | maxWorkerNum: number 282 | allowModuleSet: Set 283 | recycleTime: number 284 | resourceLimits: ResourceLimits 285 | useVmLoadDependencies: boolean 286 | } 287 | 288 | export enum WorkerMessageType { 289 | error = 'error', 290 | execute = 'execute', 291 | result = 'result', 292 | init = 'init', 293 | crash = 'crash', 294 | } 295 | 296 | export interface ExecuteMessageBody { 297 | appName: string 298 | serveName?: string 299 | executeId: string 300 | type: ServerType 301 | params: any 302 | } 303 | 304 | export interface ExecuteMessage { 305 | type: WorkerMessageType.execute 306 | data: ExecuteMessageBody 307 | } 308 | 309 | export interface ResultMessageBody { 310 | executeId: string 311 | type: ServerType 312 | result: { 313 | data: any 314 | isComplete: boolean 315 | isStream: boolean 316 | stream?: VaasWorkerStream 317 | [key: string]: any 318 | } 319 | } 320 | 321 | export interface ResultMessage { 322 | type: WorkerMessageType.result 323 | data: ResultMessageBody 324 | } 325 | 326 | // todo 要将error类型改为Error,并批量处理Error与ErrorConfig的转换 327 | export interface ErrorMessageBody { 328 | executeId?: string 329 | type: ServerType 330 | error: any 331 | } 332 | export interface ErrorMessage { 333 | type: WorkerMessageType.error 334 | data: ErrorMessageBody 335 | } 336 | 337 | export interface InitMessageBody { 338 | status: 'ok' | 'error' 339 | } 340 | 341 | export interface InitMessage { 342 | type: WorkerMessageType.init 343 | data: InitMessageBody 344 | } 345 | 346 | export type WorkerMessageBody = ExecuteMessageBody | ResultMessageBody | ErrorMessageBody | InitMessageBody 347 | export type WorkerMessage = ExecuteMessage | ResultMessage | ErrorMessage | InitMessage 348 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { type VaasConfig } from './types/server' 2 | 3 | // 仅用来校验vaas.config.js配置的类型检查 4 | export function validVaasConfig (config: VaasConfig): VaasConfig { 5 | return config 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target":"es6", 5 | "outDir": "./build/", 6 | "declaration": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "sourceMap": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": [ 13 | "src/**/*" 14 | ] 15 | } --------------------------------------------------------------------------------