From ab4de444ac37db9c98962cb0eb85b073233ae8ec Mon Sep 17 00:00:00 2001 From: axiosleo Date: Thu, 15 May 2025 21:32:41 +0800 Subject: [PATCH 01/18] fix: replace UUID generation with custom request ID function --- src/core.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core.js b/src/core.js index 2116c7f..e4b4253 100644 --- a/src/core.js +++ b/src/core.js @@ -1,7 +1,7 @@ 'use strict'; -const { v4, v5, validate } = require('uuid'); const is = require('@axiosleo/cli-tool/src/helper/is'); +const { _request_id } = require('./utils'); const resolvePathinfo = (pathinfo) => { let trace = []; @@ -200,7 +200,7 @@ const initContext = (options = {}) => { step_data: {}, method: method, pathinfo, - request_id: `${v5(v4(), !validate(app_id) ? v4() : app_id)}`, + request_id: _request_id(app_id), }; return context; }; From f68e441e3f7b9f6acfa136c7135f5e313cda80ba Mon Sep 17 00:00:00 2001 From: axiosleo Date: Thu, 15 May 2025 21:33:12 +0800 Subject: [PATCH 02/18] feat: implement socket workflow with request handling and validation --- src/apps/socket.js | 147 +++++++++++++++++++++++ src/workflows/koa.workflow.js | 43 +------ src/workflows/socket.workflow.js | 200 +++++++++++++++++++++++++++++++ 3 files changed, 350 insertions(+), 40 deletions(-) create mode 100644 src/apps/socket.js create mode 100644 src/workflows/socket.workflow.js diff --git a/src/apps/socket.js b/src/apps/socket.js new file mode 100644 index 0000000..0c535cd --- /dev/null +++ b/src/apps/socket.js @@ -0,0 +1,147 @@ +'use strict'; + +const net = require('net'); +const EventEmitter = require('events'); +const Application = require('./app'); +const { debug, printer, Workflow } = require('@axiosleo/cli-tool'); +const { _uuid, _request_id } = require('../utils'); +const { initContext } = require('../core'); +const is = require('@axiosleo/cli-tool/src/helper/is'); +const operator = require('../workflows/socket.workflow'); + +const dispatcher = ({ app, app_id, workflow, connection }) => { + return async (ctx) => { + let context = initContext({ + app, + connection, + method: ctx.method ? ctx.method.toUpperCase() : 'GET', + pathinfo: ctx.path, + app_id, + }); + context.socket = connection; + context.query = ctx.query || {}; + context.body = ctx.body || {}; + try { + await workflow.start(context); + } catch (exContext) { + context = exContext; + } + }; +}; + +/** + * @param {import('../../').SocketContext} context + */ +const handleRes = (context) => { + let response = context.response; + let data = ''; + if (response.format === 'json' && response.notResolve !== true) { + let code, message; + if (response.code) { + [code, message] = response.code.split(';'); + } + data = JSON.stringify({ + request_id: context.request_id, + timestamp: (new Date()).getTime(), + code: code || `${response.status}`, + message: message || context.response.message, + data: response.data + }); + } else { + data = response.data; + } + context.socket.write(data + '@@@@@@'); +}; + +async function ping() { + this.broadcast(123, 'ping', 0); + setTimeout(() => { + ping.call(this); + }, 1000); +} + +class SocketApplication extends Application { + constructor(options) { + super(options); + + this.event = new EventEmitter(); + this.port = this.config.port || 8081; + this.connections = {}; + this.on('response', handleRes); + this.workflow = new Workflow(operator); + } + + async start() { + const server = net.createServer((connection) => { + try { + let connection_id = _uuid(); + this.connections[connection_id] = connection; + debug.log('[Socket App]', 'Current connections:', Object.keys(this.connections).length); + this.event.emit('connection', connection); + connection.pipe(connection); + const self = this; + connection.on('data', function (data) { + try { + /** + * @example '##{"path":"/test","method":"GET","query":{"test":123}}@@' + */ + let msg = Buffer.from(data.subarray(0, data.length - 6)).toString(); + const context = JSON.parse(msg); + const callback = dispatcher({ + app: self, + app_id: self.app_id, + workflow: self.workflow, + connection + }); + process.nextTick(callback, context); + } catch (err) { + debug.log('[Socket App]', err.message); + } + }); + connection.on('end', () => { + delete this.connections[connection_id]; + debug.log('[Socket App]', 'Current connections:', Object.keys(this.connections).length); + }); + } catch (err) { + debug.log('[Socket App]', 'create socket server failed.', err); + } + }); + + server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + debug.error('[Socket App]', 'The listening port is in use.', this.port); + } else { + debug.error('[Socket App]', 'socket server error:', err); + } + }); + const self = this; + process.nextTick(() => { + ping.call(self); + }); + + server.listen(this.port, () => { + printer.info(`Server is running on port ${this.port}`); + this.event.emit('listen', this.port); + }); + } + + broadcast(data = '', msg = 'ok', code = 0, connections = null) { + data = JSON.stringify({ + request_id: _request_id(this.app_id), + timestamp: (new Date()).getTime(), + code, + message: msg, + data: data + }); + data = `${data}@@@@@@`; + if (connections === null) { + Object.keys(this.connections).map((id) => this.connections[id].write(data)); + } else if (is.array(connections)) { + connections.map((conn) => conn.write(data)); + } else if (is.object(connections)) { + Object.keys(connections).map((id) => connections[id].write(data)); + } + } +} + +module.exports = SocketApplication; diff --git a/src/workflows/koa.workflow.js b/src/workflows/koa.workflow.js index 05ebbdb..fe96642 100644 --- a/src/workflows/koa.workflow.js +++ b/src/workflows/koa.workflow.js @@ -6,7 +6,7 @@ const is = require('@axiosleo/cli-tool/src/helper/is'); const { failed, error, HttpResponse, HttpError } = require('../response'); const { _foreach } = require('@axiosleo/cli-tool/src/helper/cmd'); const { getRouteInfo } = require('../core'); -const { _str, _fixed } = require('@axiosleo/cli-tool/src/helper/str'); +const { _debug } = require('../utils'); /** * receive request @@ -115,43 +115,6 @@ async function handle(context) { } } -/** - * - * @param {import("..").KoaContext} context - */ -function showDebugInfo(context, location, error) { - const wide = 12; - printer.println('-'.repeat(30) + '[DEBUG Info]' + '-'.repeat(30)); - printer.yellow(_fixed('requestID', wide)).print(': ').println(context.request_id); - if (!error) { - printer.yellow('responseData').print(': '); - // eslint-disable-next-line no-console - console.log(context.response.data); - } - if (location && location.indexOf('node:internal') === -1) { - printer.print('response '.data).print(': ').print(location.trim().yellow).println(); - } - printer.yellow(_fixed('datetime', wide)).print(': ').println(new Date().toLocaleString()); - printer.yellow(_fixed('method', wide)).print(': ').green(context.method).println(); - printer.yellow(_fixed('path', wide)).print(': ').println(context.url); - if (!context.router) { - return; - } - const router = context.router; - ['pathinfo', 'validators'].forEach(k => { - if (is.empty(router[k])) { - return; - } - printer.yellow(_fixed(k, wide)).print(': ').println(typeof router[k] === 'object' ? JSON.stringify(router[k]) : _str(router[k])); - }); - ['query', 'params', 'body'].forEach(k => { - if (is.empty(context[k])) { - return; - } - printer.yellow(_fixed(k, wide)).print(': ').println(typeof context[k] === 'object' ? JSON.stringify(context[k]) : _str(context[k])); - }); -} - /** * set response * @param {import("..").KoaContext} context @@ -198,7 +161,7 @@ function response(context) { } context.response = response; if (error) { - showDebugInfo(context, '', error); + _debug(context, '', error); printer.red('requestError').print(': '); // eslint-disable-next-line no-console console.log(error); @@ -206,7 +169,7 @@ function response(context) { if (context.app.config.debug && !error) { let tmp = context.response.stack.split(os.EOL); let t = tmp.find((s) => !s.startsWith('Error:') && s.indexOf('node_modules') === -1); - showDebugInfo(context, t); + _debug(context, t); } context.app.emit('response', context); } diff --git a/src/workflows/socket.workflow.js b/src/workflows/socket.workflow.js new file mode 100644 index 0000000..d864230 --- /dev/null +++ b/src/workflows/socket.workflow.js @@ -0,0 +1,200 @@ +'use strict'; + +const os = require('os'); +const { printer } = require('@axiosleo/cli-tool'); +const { getRouteInfo } = require('../core'); +const { error, failed, HttpResponse, HttpError } = require('../response'); +const is = require('@axiosleo/cli-tool/src/helper/is'); +const Validator = require('validatorjs'); +const { _foreach } = require('@axiosleo/cli-tool/src/helper/cmd'); +const { _debug } = require('../utils'); + +/** + * @param {import('../../index').SocketContext} context + * @returns + */ +function receive(context) { + try { + context.app.emit('receive', context); + const router = getRouteInfo(context.app.routes, context.pathinfo, context.method); + if (!router) { + error(404, 'Not Found'); + } + context.params = router && router.params ? router.params : {}; + context.router = router; + } catch (err) { + context.response = err; + return 'response'; + } +} + +/** + * @param {import('../../index').SocketContext} context + * @returns + */ +function validate(context) { + try { + context.app.emit('validate', context); + if (context.router && context.router.validators) { + const { params, query, body } = context.router.validators; + const check = {}; + if (!is.empty(params)) { + const validation = new Validator(context.params, params.rules, params.messages || null); + validation.check(); + if (validation.fails()) { + const errors = validation.errors.all(); + check.params = errors; + } + } + if (!is.empty(query)) { + const validation = new Validator(context.query, query.rules, query.messages || null); + validation.check(); + if (validation.fails()) { + const errors = validation.errors.all(); + check.query = errors; + } + } + if (!is.empty(body)) { + const validation = new Validator(context.body, body.rules, body.messages || null); + validation.check(); + if (validation.fails()) { + const errors = validation.errors.all(); + check.body = errors; + } + } + if (!is.empty(check)) { + failed(check, '400;Bad Request Data', 400); + } + } + } catch (err) { + context.response = err; + return 'response'; + } +} + +/** + * @param {import('../../index').SocketContext} context + * @returns + */ +async function middleware(context) { + try { + context.app.emit('middleware', context); + // exec middleware by routes configuration + if (context.router && context.router.middlewares && context.router.middlewares.length > 0) { + await _foreach(context.router.middlewares, async (middleware) => { + await middleware(context); + }); + } + } catch (err) { + context.response = err; + return 'response'; + } +} + +/** + * @param {import('../../index').SocketContext} context + * @returns + */ +async function handle(context) { + try { + context.app.emit('handle', context); + if (context.router && context.router.handlers + && context.router.handlers.length > 0) { + await _foreach(context.router.handlers, async (handler) => { + await handler(context); + }); + } + } catch (err) { + context.response = err; + return; + } +} + +/** + * @param {import('../../index').SocketContext} context + * @returns + */ +function response(context) { + if (!context.response) { + return; + } + if (!context.response && context.curr && context.curr.error) { + context.response = context.curr.error || new Error('unknown error'); + } + let response; + let error; + if (context.response instanceof HttpResponse) { + response = context.response; + } else if (context.response instanceof HttpError) { + response = new HttpResponse({ + format: 'json', + status: context.response.status, + message: context.response.message, + data: {} + }); + } else if (context.app.config.debug) { + error = context.response; + response = new HttpResponse({ + format: 'json', + status: 500, + data: { + code: 500, + message: 'Internal Server Error', + data: { + code: context.response.code, + msg: context.response.message, + stack: context.response.stack, + }, + } + }); + } else { + error = context.response; + response = new HttpResponse({ + status: 500, + data: 'Internal Server Error' + }); + } + context.response = response; + if (error) { + _debug(context, '', error); + printer.red('requestError').print(': '); + // eslint-disable-next-line no-console + console.log(error); + } + if (context.app.config.debug && !error) { + let tmp = context.response.stack.split(os.EOL); + let t = tmp.find((s) => !s.startsWith('Error:') && s.indexOf('node_modules') === -1); + _debug(context, t); + } + context.app.emit('response', context); +} + +/** + * @param {import('../../index').SocketContext} context + * @returns + */ +async function after(context) { + try { + context.app.emit('request_end', context); + if (context.router && context.router.afters && context.router.afters.length > 0) { + await _foreach(context.router.afters, async (after) => { + try { + await after(context); + } catch (err) { + context.app.emit('after_error', context, err); + } + }); + } + } catch (err) { + context.response = err; + } +} + +module.exports = { + receive, + validate, + middleware, + handle, + response, + after +}; From 68c56e1fc0a51a309458b5f1b459d1bab94df14b Mon Sep 17 00:00:00 2001 From: axiosleo Date: Thu, 15 May 2025 21:33:16 +0800 Subject: [PATCH 03/18] fix: correct SocketApplication import path in index.js --- src/apps/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/index.js b/src/apps/index.js index 7b8687a..99aa884 100644 --- a/src/apps/index.js +++ b/src/apps/index.js @@ -2,7 +2,7 @@ const Application = require('./app'); const KoaApplication = require('./koa'); -const SocketApplication = require('./koa'); +const SocketApplication = require('./socket'); module.exports = { Application, From a0c6379f6e81953d12df31a063abaf6eaa90948b Mon Sep 17 00:00:00 2001 From: axiosleo Date: Thu, 29 May 2025 09:35:39 +0800 Subject: [PATCH 04/18] feat: add utility functions and SocketClient class for enhanced socket communication --- src/utils.js | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/utils.js diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..44f0dbe --- /dev/null +++ b/src/utils.js @@ -0,0 +1,172 @@ +'use strict'; + +const net = require('net'); +const { printer, debug } = require('@axiosleo/cli-tool'); +const is = require('@axiosleo/cli-tool/src/helper/is'); +const { _fixed, _str } = require('@axiosleo/cli-tool/src/helper/str'); +const EventEmitter = require('events'); +const { v4, v5, validate } = require('uuid'); + +function _uuid() { + return v4(); +} + +function _uuid_salt(salt = '') { + return `${v5(v4(), !validate(salt) ? v4() : salt)}`; +} + +/** + * @param {import("..").KoaContext} context + */ +function _debug(context, location, error) { + const wide = 12; + printer.println('-'.repeat(30) + '[DEBUG Info]' + '-'.repeat(30)); + printer.yellow(_fixed('requestID', wide)).print(': ').println(context.request_id); + if (!error) { + printer.yellow('responseData').print(': '); + // eslint-disable-next-line no-console + console.log(context.response.data); + } + if (location && location.indexOf('node:internal') === -1) { + printer.print('response '.data).print(': ').print(location.trim().yellow).println(); + } + printer.yellow(_fixed('datetime', wide)).print(': ').println(new Date().toLocaleString()); + printer.yellow(_fixed('method', wide)).print(': ').green(context.method).println(); + printer.yellow(_fixed('path', wide)).print(': ').println(context.url); + if (!context.router) { + return; + } + const router = context.router; + ['pathinfo', 'validators'].forEach(k => { + if (is.empty(router[k])) { + return; + } + printer.yellow(_fixed(k, wide)).print(': ').println(typeof router[k] === 'object' ? JSON.stringify(router[k]) : _str(router[k])); + }); + ['query', 'params', 'body'].forEach(k => { + if (is.empty(context[k])) { + return; + } + printer.yellow(_fixed(k, wide)).print(': ').println(typeof context[k] === 'object' ? JSON.stringify(context[k]) : _str(context[k])); + }); +} + +class SocketClient { + constructor(options = {}) { + this.options = Object.assign({ + port: 8081, + host: 'localhost', + name: 'default', + }, options); + this.event = new EventEmitter(); + this.client = net.connect(this.options); + this.client.on('connect', () => { + debug.log('connection success'); + this.event.emit('connect'); + }); + this.cache = []; + this.client.on('data', (data) => { + let str = data.toString(); + if (str.indexOf('@@@@@@') === -1) { + debug.log('no end tag, push to cache', str); + this.cache.push(data); + return; + } + this.cache.push(data.subarray(0, data.indexOf('@@@@@@'))); + str = Buffer.concat(this.cache).toString(); + let temp = data.subarray(data.indexOf('@@@@@@') + 6); + this.cache = temp.length > 0 ? [temp] : []; + if (str.indexOf('@@@@@@') !== -1) { + str = str.substring(0, str.indexOf('@@@@@@')); + } + if (str.length === 0) { + return this.event.emit('error', new Error('empty data')); + } + this.event.emit('data', JSON.parse(str)); + }); + this.client.on('error', (err) => { + debug.log('connection error', err.message); + // this.event.emit('error', err); + }); + const self = this; + this.client.on('end', function () { + debug.log('connection end, will reconnect', self.options); + }); + } + + reconnect() { + if (this.client) { + this.client.destroy(); + } + this.client = net.connect(this.options); + this.client.on('connect', () => { + debug.log('connection success'); + this.event.emit('connect'); + }); + this.client.on('data', (data) => { + let str = data.subarray(0, data.length - 6).toString(); + this.event.emit('data', JSON.parse(str)); + }); + this.client.on('error', (err) => { + debug.log('connection error', err.message); + this.event.emit('error', err); + }); + this.client.on('end', function () { + debug.log('connection end'); + }); + this.event.emit('reconnect'); + } + + async send(method, pathinfo, query = {}, body = {}) { + const methods = ['get', 'post', 'put', 'delete', 'patch']; + if (!is.string(method) || !methods.includes(method.toLowerCase())) { + throw new Error('method must be one of get, post, put, delete, patch'); + } + if (!is.string(pathinfo)) { + throw new Error('pathinfo must be a string'); + } + if (!is.object(query)) { + throw new Error('query must be an object'); + } + if (!is.object(body)) { + throw new Error('body must be an object'); + } + if (this.client && this.client.destroyed) { + this.reconnect(); + } + if (!this.client) { + throw new Error('socket client is not connected'); + } + const self = this; + return new Promise((resolve, reject) => { + if (this.client) { + const bufferBody = Buffer.from(`${Buffer.from(JSON.stringify({ + path: pathinfo, + method, + query, + body + }).toString('base64'))}@@@@@@`); + this.event.on('data', (data) => { + debug.log('data123', { data }); + resolve(data); + }); + this.client.write(bufferBody, (e) => { + if (e) { + self.event.emit('error', e); + reject(e); + } + }); + } else { + reject(new Error('socket client is not connected')); + } + }); + } +} + +module.exports = { + _uuid, + _debug, + _uuid_salt, + + SocketClient +}; From 6d935ffc54689be6aac4ceb4161aa3a229d1fbf8 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Thu, 29 May 2025 09:35:51 +0800 Subject: [PATCH 05/18] fix: update request ID generation to use _uuid_salt for improved uniqueness --- src/core.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core.js b/src/core.js index e4b4253..9e9f629 100644 --- a/src/core.js +++ b/src/core.js @@ -1,7 +1,7 @@ 'use strict'; const is = require('@axiosleo/cli-tool/src/helper/is'); -const { _request_id } = require('./utils'); +const { _uuid_salt } = require('./utils'); const resolvePathinfo = (pathinfo) => { let trace = []; @@ -200,7 +200,7 @@ const initContext = (options = {}) => { step_data: {}, method: method, pathinfo, - request_id: _request_id(app_id), + request_id: _uuid_salt(app_id), }; return context; }; From 85f8068cf9aaf1ac0c10da7f7a4dc141e7f94e39 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Wed, 4 Jun 2025 15:16:52 +0800 Subject: [PATCH 06/18] feat: enhance VSCode launch configuration and add socket client/server examples --- .vscode/launch.json | 20 ++++++++++++++++-- examples/socket.client.js | 41 ++++++++++++++++++++++++++++++++++++ examples/socket.server.js | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 examples/socket.client.js create mode 100644 examples/socket.server.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 6cc9e7c..d3cc773 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ { "type": "node", "request": "launch", - "name": "Launch Program", + "name": "Web api", "skipFiles": ["/**"], "program": "${workspaceFolder}/bin/koapp.js", "args": ["http", "-p", "8080"] @@ -15,10 +15,26 @@ { "type": "node", "request": "launch", - "name": "Test Program", + "name": "Test server", "skipFiles": ["/**"], "program": "${workspaceFolder}/tests/bootstrap.js", "args": [] + }, + { + "type": "node", + "request": "launch", + "name": "Socket Client", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/examples/socket.client.js", + "args": [] + }, + { + "type": "node", + "request": "launch", + "name": "Socket Server", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/examples/socket.server.js", + "args": [] } ] } diff --git a/examples/socket.client.js b/examples/socket.client.js new file mode 100644 index 0000000..f0d581e --- /dev/null +++ b/examples/socket.client.js @@ -0,0 +1,41 @@ +'use strict'; + +const { debug } = require('@axiosleo/cli-tool'); +const { _sleep } = require('@axiosleo/cli-tool/src/helper/cmd'); +const { SocketClient } = require('..'); + +let client = null; + +/** + * @returns {SocketClient} + */ +function getClient() { + if (!client) { + client = new SocketClient(); + } + return client; +} + +async function main() { + try { + const client = getClient(); + await client.send('get', '/api/test/123', { + test: 123 + }, { + data: { + t: 1 + } + }); + } catch (err) { + debug.log('error', err.code); + } + + // await _sleep(3000); + // process.nextTick(main); +} + +main().then(() => { + // debug.log('done'); +}).catch((err) => { + // debug.log(err); +}); diff --git a/examples/socket.server.js b/examples/socket.server.js new file mode 100644 index 0000000..94b568c --- /dev/null +++ b/examples/socket.server.js @@ -0,0 +1,44 @@ +'use strict'; + +const { Router } = require('..'); +const SocketApplication = require('../src/apps/socket'); +const { success } = require('../src/response'); + +// const { debug } = require('@axiosleo/cli-tool'); + +const root = new Router(null, { + middlewares: [async (context) => { + // debug.log(`[${context.app_id}] ${context.method}: ${context.router.pathinfo}`); + }], + afters: [async (context) => { + // debug.log(context); + }] +}); + +root.get('/api/test/{:id}', async (context) => { + // debug.log(context.connection.write(JSON.stringify({ test: 123 }))); + success({ + query: context.query, + body: context.body, + test: 'Hello World!' + }); +}); + +root.any('/***', async (context) => { + // debug.log(context.connection.write(JSON.stringify({ test: 123 }))); + success({ + query: context.query, + body: context.body + }); +}); + +const app = new SocketApplication({ + routers: [root], + ping: { + open: true, + interval: 1000 * 10, + data: 'this is a ping message.' + } +}); + +app.start(); From fc3c023a91f3a1b6cc9a519a8a44782bc60a5598 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Wed, 4 Jun 2025 15:22:15 +0800 Subject: [PATCH 07/18] feat: enhance socket ping functionality and improve request ID generation --- src/apps/socket.js | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/apps/socket.js b/src/apps/socket.js index 0c535cd..5afe78a 100644 --- a/src/apps/socket.js +++ b/src/apps/socket.js @@ -4,10 +4,12 @@ const net = require('net'); const EventEmitter = require('events'); const Application = require('./app'); const { debug, printer, Workflow } = require('@axiosleo/cli-tool'); -const { _uuid, _request_id } = require('../utils'); +const { _uuid_salt } = require('../utils'); const { initContext } = require('../core'); const is = require('@axiosleo/cli-tool/src/helper/is'); const operator = require('../workflows/socket.workflow'); +const { _assign } = require('@axiosleo/cli-tool/src/helper/obj'); +const { _sleep } = require('@axiosleo/cli-tool/src/helper/cmd'); const dispatcher = ({ app, app_id, workflow, connection }) => { return async (ctx) => { @@ -53,11 +55,12 @@ const handleRes = (context) => { context.socket.write(data + '@@@@@@'); }; -async function ping() { - this.broadcast(123, 'ping', 0); - setTimeout(() => { - ping.call(this); - }, 1000); +async function ping(data, interval) { + this.broadcast(data, 'ping', 0); + await _sleep(interval); + process.nextTick(() => { + ping.call(this, data, interval); + }); } class SocketApplication extends Application { @@ -69,12 +72,18 @@ class SocketApplication extends Application { this.connections = {}; this.on('response', handleRes); this.workflow = new Workflow(operator); + this.ping = {}; + _assign(this.ping, { + open: false, + interval: 1000 * 60 * 5, + data: 'this is a ping message' + }, this.config.ping || {}); } async start() { const server = net.createServer((connection) => { try { - let connection_id = _uuid(); + let connection_id = _uuid_salt('connect:' + this.app_id); this.connections[connection_id] = connection; debug.log('[Socket App]', 'Current connections:', Object.keys(this.connections).length); this.event.emit('connection', connection); @@ -83,7 +92,7 @@ class SocketApplication extends Application { connection.on('data', function (data) { try { /** - * @example '##{"path":"/test","method":"GET","query":{"test":123}}@@' + * @example '{"path":"/test","method":"GET","query":{"test":123}}@@@@@@' */ let msg = Buffer.from(data.subarray(0, data.length - 6)).toString(); const context = JSON.parse(msg); @@ -114,10 +123,13 @@ class SocketApplication extends Application { debug.error('[Socket App]', 'socket server error:', err); } }); - const self = this; - process.nextTick(() => { - ping.call(self); - }); + if (this.ping.open) { + const self = this; + printer.info('[Socket App] ping is open.'); + process.nextTick(() => { + ping.call(self, self.ping.data, self.ping.interval); + }); + } server.listen(this.port, () => { printer.info(`Server is running on port ${this.port}`); @@ -127,7 +139,7 @@ class SocketApplication extends Application { broadcast(data = '', msg = 'ok', code = 0, connections = null) { data = JSON.stringify({ - request_id: _request_id(this.app_id), + request_id: _uuid_salt(this.app_id), timestamp: (new Date()).getTime(), code, message: msg, From 8c481df3251ea31e603a169fc2d75d9d8b2390c4 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Wed, 4 Jun 2025 15:24:58 +0800 Subject: [PATCH 08/18] feat: add SocketContext and SocketApplication classes for enhanced socket support --- index.d.ts | 37 +++++++++++++++++++++++++++++++++++-- index.js | 8 ++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index 52c43f7..2f6f56f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,6 +5,7 @@ import { IncomingHttpHeaders } from "http"; import * as Koa from "koa"; import * as session from "koa-session"; import * as KoaStaticServer from "koa-static-server"; +import type { Socket } from "net"; import { Transform } from "stream"; import { ErrorMessages, Rules, Validator } from "validatorjs"; @@ -168,9 +169,11 @@ interface AppContext extends Context { app_id: string; method?: string; pathinfo?: string; + params?: any; config: AppConfiguration; request_id: string; - router?: RouterInfo | null; + router?: RouterInfo; + response?: HttpResponse | HttpError; } interface IKoaSSEvent { @@ -193,7 +196,6 @@ interface KoaContext extends AppContext { router?: RouterInfo | null; access_key_id?: string; app_key?: string; - params?: any; body?: any; file?: File | null; files?: File[]; @@ -330,6 +332,10 @@ export declare class KoaApplication extends Application { start(): Promise; } +export interface SocketContext extends AppContext { + socket: Socket; +} + export declare class Model { constructor(obj?: { [key: string]: any }, rules?: Rules, msg?: ErrorMessages); @@ -358,3 +364,30 @@ export function initContext< pathinfo?: string; app_id?: string; }): F & { app: T }; + +export declare class SocketClient { + options: { + port: number; + host: string; + name?: string; + }; + event: EventEmitter; + client: Socket; + constructor(socket: Socket, app_id: string); + send(data: any): void; + close(): void; +} + +export type SocketAppConfiguration = AppConfiguration & { + port: number; + ping?: { + open?: boolean; + interval?: number; + data?: any; + }; +}; + +export declare class SocketApplication extends Application { + constructor(config: SocketAppConfiguration); + start(): Promise; +} diff --git a/index.js b/index.js index 86cca8f..4d26620 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ 'use strict'; -const { Application, KoaApplication } = require('./src/apps'); +const { Application, KoaApplication, SocketApplication } = require('./src/apps'); const Controller = require('./src/controller'); const { Router } = require('./src/router'); const response = require('./src/response'); @@ -9,11 +9,13 @@ const { KoaSSEMiddleware } = require('./src/middlewares/sse'); const { initContext } = require('./src/core'); const multer = require('@koa/multer'); const session = require('koa-session'); +const { SocketClient } = require('./src/utils'); module.exports = { Controller, Application, KoaApplication, + SocketApplication, Model, Router, @@ -26,5 +28,7 @@ module.exports = { // functions ...response, - initContext + initContext, + + SocketClient }; From 75dc8668bb5cdaa721f4009128cae715953d6045 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Wed, 4 Jun 2025 15:25:13 +0800 Subject: [PATCH 09/18] fix: correct error handling in socket client by enabling error logging --- examples/socket.client.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/socket.client.js b/examples/socket.client.js index f0d581e..ce31abd 100644 --- a/examples/socket.client.js +++ b/examples/socket.client.js @@ -30,12 +30,12 @@ async function main() { debug.log('error', err.code); } - // await _sleep(3000); - // process.nextTick(main); + await _sleep(3000); + process.nextTick(main); } main().then(() => { // debug.log('done'); }).catch((err) => { - // debug.log(err); + debug.log(err); }); From 8a5b900564eb2c23e0a126237d9796117702523d Mon Sep 17 00:00:00 2001 From: axiosleo Date: Mon, 7 Jul 2025 16:37:33 +0800 Subject: [PATCH 10/18] feat: add WebSocketApplication class and update index.js to include WebSocket support --- index.js | 8 +- src/apps/index.js | 4 +- src/apps/websocket.js | 167 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 src/apps/websocket.js diff --git a/index.js b/index.js index 4d26620..ce464bf 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ 'use strict'; -const { Application, KoaApplication, SocketApplication } = require('./src/apps'); const Controller = require('./src/controller'); const { Router } = require('./src/router'); const response = require('./src/response'); @@ -10,12 +9,19 @@ const { initContext } = require('./src/core'); const multer = require('@koa/multer'); const session = require('koa-session'); const { SocketClient } = require('./src/utils'); +const { + Application, + KoaApplication, + SocketApplication, + WebSocketApplication +} = require('./src/apps'); module.exports = { Controller, Application, KoaApplication, SocketApplication, + WebSocketApplication, Model, Router, diff --git a/src/apps/index.js b/src/apps/index.js index 99aa884..c406b7f 100644 --- a/src/apps/index.js +++ b/src/apps/index.js @@ -3,9 +3,11 @@ const Application = require('./app'); const KoaApplication = require('./koa'); const SocketApplication = require('./socket'); +const WebSocketApplication = require('./websocket'); module.exports = { Application, KoaApplication, - SocketApplication + SocketApplication, + WebSocketApplication }; diff --git a/src/apps/websocket.js b/src/apps/websocket.js new file mode 100644 index 0000000..015e5cf --- /dev/null +++ b/src/apps/websocket.js @@ -0,0 +1,167 @@ +'use strict'; + +const { WebSocketServer } = require('ws'); +const EventEmitter = require('events'); +const Application = require('./app'); +const { debug, printer, Workflow } = require('@axiosleo/cli-tool'); +const { _uuid_salt } = require('../utils'); +const { initContext } = require('../core'); +const is = require('@axiosleo/cli-tool/src/helper/is'); +const operator = require('../workflows/socket.workflow'); +const { _assign } = require('@axiosleo/cli-tool/src/helper/obj'); +const { _sleep } = require('@axiosleo/cli-tool/src/helper/cmd'); + +/** + * + * @param {{request: import('http').IncomingMessage}} param0 + * @returns + */ +const dispatcher = ({ app, app_id, workflow, connection, request }) => { + return async (ctx) => { + const url = new URL(request.url, `ws://localhost:${app.port}`); + let context = initContext({ + app, + method: request.method ? request.method.toUpperCase() : 'GET', + pathinfo: url.pathname, + app_id, + }); + context.socket = connection; + context.query = connection.connectionQuery || {}; + context.body = ctx || {}; + context.headers = request.headers; + try { + await workflow.start(context); + } catch (exContext) { + context = exContext; + } + }; +}; + +/** + * @param {import('../../').SocketContext} context + */ +const handleRes = (context) => { + let response = context.response; + let data = ''; + if (response.format === 'json' && response.notResolve !== true) { + let code, message; + if (response.code) { + [code, message] = response.code.split(';'); + } + data = JSON.stringify({ + request_id: context.request_id, + timestamp: (new Date()).getTime(), + code: code || `${response.status}`, + message: message || context.response.message, + data: response.data + }); + } else { + data = response.data; + } + context.socket.send(data); +}; + +async function ping(data, interval) { + this.broadcast(data, 'ping', 0); + await _sleep(interval); + process.nextTick(() => { + ping.call(this, data, interval); + }); +} + +class WebSocketApplication extends Application { + constructor(options) { + super(options); + + this.event = new EventEmitter(); + this.port = this.config.port || 8081; + this.connections = {}; + this.on('response', handleRes); + this.workflow = new Workflow(operator); + this.ping = {}; + _assign(this.ping, { + open: false, + interval: 1000 * 60 * 5, + data: 'this is a ping message' + }, this.config.ping || {}); + } + + async start() { + const wss = new WebSocketServer({ port: this.port }); + printer.info(`Server is running on port ${this.port}`); + this.event.emit('listen', this.port); + const self = this; + wss.on('connection', (ws, request) => { + let connection_id = _uuid_salt('connect:' + this.app_id); + this.connections[connection_id] = ws; + + debug.log('[WebSocket App]', 'Current connections:', Object.keys(this.connections).length); + this.event.emit('connection', ws, request); + + ws.on('message', (data) => { + try { + /** + * @example '{"path":"/test","method":"GET","query":{"test":123}}' + */ + let msg = Buffer.from(data).toString(); + const context = JSON.parse(msg); + const callback = dispatcher({ + app: self, + app_id: self.app_id, + workflow: self.workflow, + connection: ws, + request + }); + process.nextTick(callback, context); + } catch (err) { + debug.log('[Socket App]', err.message); + } + }); + ws.on('error', (err) => { + debug.error('[Socket App]', 'socket server error:', err); + }); + ws.on('close', () => { + delete this.connections[connection_id]; + debug.log('[Socket App]', 'Current connections:', Object.keys(this.connections).length); + }); + }); + + wss.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + debug.error('[Socket App]', 'The listening port is in use.', this.port); + } else { + debug.error('[Socket App]', 'socket server error:', err); + } + }); + + if (this.ping.open) { + const self = this; + printer.info('[Socket App] ping is open.'); + process.nextTick(() => { + ping.call(self, self.ping.data, self.ping.interval); + }); + } + } + + broadcast(data = '', msg = 'ok', code = 0, connections = null) { + if (connections === null && is.empty(this.connections)) { + return; + } + data = JSON.stringify({ + request_id: _uuid_salt(this.app_id), + timestamp: (new Date()).getTime(), + code, + message: msg, + data: data + }); + if (connections === null) { + Object.keys(this.connections).map((id) => this.connections[id].send(data)); + } else if (is.array(connections)) { + connections.map((conn) => conn.send(data)); + } else if (is.object(connections)) { + Object.keys(connections).map((id) => connections[id].send(data)); + } + } +} + +module.exports = WebSocketApplication; From 3becef8e26f35242dbf65bc2bcecf136adb8fa0e Mon Sep 17 00:00:00 2001 From: axiosleo Date: Mon, 7 Jul 2025 16:37:46 +0800 Subject: [PATCH 11/18] feat: implement API router and WebSocket server with HTML client for enhanced WebSocket communication --- examples/api.router.js | 30 ++++++ examples/socket.server.js | 30 +----- examples/socket.web.html | 192 +++++++++++++++++++++++++++++++++++ examples/websocket.server.js | 14 +++ 4 files changed, 237 insertions(+), 29 deletions(-) create mode 100644 examples/api.router.js create mode 100644 examples/socket.web.html create mode 100644 examples/websocket.server.js diff --git a/examples/api.router.js b/examples/api.router.js new file mode 100644 index 0000000..7105011 --- /dev/null +++ b/examples/api.router.js @@ -0,0 +1,30 @@ +const { success } = require('../src/response'); + +const { Router } = require('..'); +const { debug } = require('@axiosleo/cli-tool'); + +const root = new Router(null, { + middlewares: [async (context) => { + debug.log(`[${context.app_id}] ${context.method}: ${context.router.pathinfo}`); + }], + afters: [async (context) => { + // debug.log(context); + }] +}); + +root.get('/api/test/{:id}', async (context) => { + success({ + query: context.query, + body: context.body, + test: context.params.id + }); +}); + +root.any('/***', async (context) => { + success({ + query: context.query, + body: context.body + }); +}); + +module.exports = root; diff --git a/examples/socket.server.js b/examples/socket.server.js index 94b568c..2e2ad9a 100644 --- a/examples/socket.server.js +++ b/examples/socket.server.js @@ -1,36 +1,8 @@ 'use strict'; -const { Router } = require('..'); const SocketApplication = require('../src/apps/socket'); -const { success } = require('../src/response'); -// const { debug } = require('@axiosleo/cli-tool'); - -const root = new Router(null, { - middlewares: [async (context) => { - // debug.log(`[${context.app_id}] ${context.method}: ${context.router.pathinfo}`); - }], - afters: [async (context) => { - // debug.log(context); - }] -}); - -root.get('/api/test/{:id}', async (context) => { - // debug.log(context.connection.write(JSON.stringify({ test: 123 }))); - success({ - query: context.query, - body: context.body, - test: 'Hello World!' - }); -}); - -root.any('/***', async (context) => { - // debug.log(context.connection.write(JSON.stringify({ test: 123 }))); - success({ - query: context.query, - body: context.body - }); -}); +const root = require('./api.router'); const app = new SocketApplication({ routers: [root], diff --git a/examples/socket.web.html b/examples/socket.web.html new file mode 100644 index 0000000..590e79b --- /dev/null +++ b/examples/socket.web.html @@ -0,0 +1,192 @@ + + + + + + WebSocket Example + + + +
+

WebSocket Example

+ +
Disconnected
+ +
+ + + +
+ +
+ +
+ + +
+
+ + + + diff --git a/examples/websocket.server.js b/examples/websocket.server.js new file mode 100644 index 0000000..8831e7f --- /dev/null +++ b/examples/websocket.server.js @@ -0,0 +1,14 @@ +const { WebSocketApplication } = require('../src/apps'); +const root = require('./api.router'); + +const app = new WebSocketApplication({ + routers: [root], + port: 8081, + ping: { + open: false, + interval: 1000 * 3, + data: 'this is a ping message' + } +}); + +app.start(); From ae35a531fd02b555e395f4f12a9d0fdbefffc458 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Mon, 7 Jul 2025 16:37:53 +0800 Subject: [PATCH 12/18] feat: add ws dependency for WebSocket support in the project --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7a5ea5f..9312177 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "koa-static-server": "^1.5.2", "multer": "^1.4.5-lts.1", "uuid": "^9.0.1", - "validatorjs": "^3.22.1" + "validatorjs": "^3.22.1", + "ws": "^8.18.2" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", From 6dcbdac4dca22c1cef32e0bfb6ad9020edbbc402 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Mon, 7 Jul 2025 16:41:59 +0800 Subject: [PATCH 13/18] feat: update VSCode launch configuration with clearer names and add WebSocket server example --- .vscode/launch.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d3cc773..e8452fb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ { "type": "node", "request": "launch", - "name": "Web api", + "name": "Web server", "skipFiles": ["/**"], "program": "${workspaceFolder}/bin/koapp.js", "args": ["http", "-p", "8080"] @@ -15,7 +15,7 @@ { "type": "node", "request": "launch", - "name": "Test server", + "name": "Test web server", "skipFiles": ["/**"], "program": "${workspaceFolder}/tests/bootstrap.js", "args": [] @@ -23,7 +23,7 @@ { "type": "node", "request": "launch", - "name": "Socket Client", + "name": "Example: Socket Client", "skipFiles": ["/**"], "program": "${workspaceFolder}/examples/socket.client.js", "args": [] @@ -31,10 +31,18 @@ { "type": "node", "request": "launch", - "name": "Socket Server", + "name": "Example: Socket Server", "skipFiles": ["/**"], "program": "${workspaceFolder}/examples/socket.server.js", "args": [] + }, + { + "type": "node", + "request": "launch", + "name": "Example: WebSocket Server", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/examples/websocket.server.js", + "args": [] } ] } From df4ee004d2a0009d9c8f18a158c07fdd6312d794 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Mon, 7 Jul 2025 16:42:54 +0800 Subject: [PATCH 14/18] feat: enhance API router logging by including query, body, and test parameters for better debugging --- examples/api.router.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/api.router.js b/examples/api.router.js index 7105011..caf709e 100644 --- a/examples/api.router.js +++ b/examples/api.router.js @@ -8,7 +8,11 @@ const root = new Router(null, { debug.log(`[${context.app_id}] ${context.method}: ${context.router.pathinfo}`); }], afters: [async (context) => { - // debug.log(context); + debug.log({ + query: context.query, + body: context.body, + test: context.params.id + }); }] }); From c8869cdc38fc2b8b6df130cd255867a50dcd364f Mon Sep 17 00:00:00 2001 From: axiosleo Date: Mon, 7 Jul 2025 17:11:33 +0800 Subject: [PATCH 15/18] feat: update WebSocketApplication to accept custom websocket options and improve server initialization --- src/apps/websocket.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/apps/websocket.js b/src/apps/websocket.js index 015e5cf..efb53b4 100644 --- a/src/apps/websocket.js +++ b/src/apps/websocket.js @@ -70,6 +70,10 @@ async function ping(data, interval) { } class WebSocketApplication extends Application { + /** + * + * @param {import('../../').WebSocketAppConfiguration} options + */ constructor(options) { super(options); @@ -84,10 +88,12 @@ class WebSocketApplication extends Application { interval: 1000 * 60 * 5, data: 'this is a ping message' }, this.config.ping || {}); + delete options.ping; + this.websocketOptions = options; } async start() { - const wss = new WebSocketServer({ port: this.port }); + const wss = new WebSocketServer(this.websocketOptions); printer.info(`Server is running on port ${this.port}`); this.event.emit('listen', this.port); const self = this; From 7251d062fc3e95f8d1f92cf3aa70a34c0a93d9ad Mon Sep 17 00:00:00 2001 From: axiosleo Date: Tue, 8 Jul 2025 08:36:41 +0800 Subject: [PATCH 16/18] refactor: improve connection handling --- src/apps/socket.js | 5 ++++- src/apps/websocket.js | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/apps/socket.js b/src/apps/socket.js index 5afe78a..b329d9b 100644 --- a/src/apps/socket.js +++ b/src/apps/socket.js @@ -137,7 +137,7 @@ class SocketApplication extends Application { }); } - broadcast(data = '', msg = 'ok', code = 0, connections = null) { + broadcast(data = '', msg = 'ok', code = 0, connections = []) { data = JSON.stringify({ request_id: _uuid_salt(this.app_id), timestamp: (new Date()).getTime(), @@ -147,6 +147,9 @@ class SocketApplication extends Application { }); data = `${data}@@@@@@`; if (connections === null) { + if (is.empty(this.connections)) { + return; + } Object.keys(this.connections).map((id) => this.connections[id].write(data)); } else if (is.array(connections)) { connections.map((conn) => conn.write(data)); diff --git a/src/apps/websocket.js b/src/apps/websocket.js index efb53b4..4816f5c 100644 --- a/src/apps/websocket.js +++ b/src/apps/websocket.js @@ -149,10 +149,7 @@ class WebSocketApplication extends Application { } } - broadcast(data = '', msg = 'ok', code = 0, connections = null) { - if (connections === null && is.empty(this.connections)) { - return; - } + broadcast(data = '', msg = 'ok', code = 0, connections = []) { data = JSON.stringify({ request_id: _uuid_salt(this.app_id), timestamp: (new Date()).getTime(), @@ -161,6 +158,9 @@ class WebSocketApplication extends Application { data: data }); if (connections === null) { + if (is.empty(this.connections)) { + return; + } Object.keys(this.connections).map((id) => this.connections[id].send(data)); } else if (is.array(connections)) { connections.map((conn) => conn.send(data)); From e4a74b60ee3a8158370568b47a2c51a644a9208d Mon Sep 17 00:00:00 2001 From: axiosleo Date: Tue, 8 Jul 2025 09:26:19 +0800 Subject: [PATCH 17/18] feat: enhance types --- index.d.ts | 1052 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 960 insertions(+), 92 deletions(-) diff --git a/index.d.ts b/index.d.ts index 2f6f56f..58882d5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,7 +8,16 @@ import * as KoaStaticServer from "koa-static-server"; import type { Socket } from "net"; import { Transform } from "stream"; import { ErrorMessages, Rules, Validator } from "validatorjs"; +import type { ServerOptions, WebSocket } from "ws"; +// ======================================== +// Status Code Types +// ======================================== + +/** + * Predefined status codes with format "code;message" + * Used for standardized API responses + */ type StatusCode = | string | "000;Unknown Error" @@ -22,6 +31,14 @@ type StatusCode = | "501;Failed" | "409;Data Already Exists"; +// ======================================== +// HTTP Method Types +// ======================================== + +/** + * HTTP methods supported by the framework + * Includes both uppercase and lowercase variants + */ type HttpMethod = | "ANY" | "GET" @@ -45,38 +62,104 @@ type HttpMethod = | "connect" | string; -export function response( - data: unknown, +// ======================================== +// Response Functions +// ======================================== + +/** + * Send a response with data, status code, and optional headers + * @template T Type of response data + * @param data Response data + * @param code Status code in format "code;message" + * @param httpStatus HTTP status code (default: 200) + * @param headers Optional response headers + */ +export function response( + data: T, code?: StatusCode, httpStatus?: number, headers?: Record ): void; -export function result( - data: unknown, + +/** + * Send a result response with data and optional headers + * @template T Type of response data + * @param data Response data + * @param httpStatus HTTP status code (default: 200) + * @param headers Optional response headers + */ +export function result( + data: T, httpStatus?: number, headers?: Record ): void; -export function success(data?: unknown, headers?: Record): void; -export function failed( - data?: unknown, + +/** + * Send a success response with optional data and headers + * @template T Type of response data + * @param data Optional response data + * @param headers Optional response headers + */ +export function success( + data?: T, + headers?: Record +): void; + +/** + * Send a failed response with error data and status + * @template T Type of response data + * @param data Error data + * @param code Status code in format "code;message" + * @param httpStatus HTTP status code (default: 500) + * @param headers Optional response headers + */ +export function failed( + data?: T, code?: StatusCode, httpStatus?: number, headers?: Record ): void; + +/** + * Send an error response with HTTP status and message + * @param httpStatus HTTP status code + * @param msg Error message + * @param headers Optional response headers + */ export function error( httpStatus: number, msg: string, headers?: Record ): void; + +/** + * Log data to console (development utility) + * @param data Data to log + */ export function log(...data: any): void; +// ======================================== +// HTTP Response Classes +// ======================================== + +/** + * Configuration for HTTP response + */ export interface HttpResponseConfig { + /** HTTP status code */ status?: number; + /** Response headers */ headers?: IncomingHttpHeaders; + /** Response data */ data?: unknown; + /** Response format */ format?: "json" | "text"; } +/** + * HTTP response class for structured responses + * Extends Error to work with error handling middleware + */ export declare class HttpResponse extends Error { public readonly status: number; public readonly headers: IncomingHttpHeaders; @@ -84,6 +167,10 @@ export declare class HttpResponse extends Error { constructor(config?: HttpResponseConfig); } +/** + * HTTP error class for error responses + * Extends Error to work with error handling middleware + */ export declare class HttpError extends Error { public readonly status: number; public readonly headers: IncomingHttpHeaders; @@ -95,21 +182,28 @@ export declare class HttpError extends Error { ); } +// ======================================== +// Controller Interface and Class +// ======================================== + +/** + * Interface defining controller response methods + */ interface ControllerInterface { - response( - data: unknown, + response( + data: T, code?: StatusCode, status?: number, headers?: Record ): void; - result( - data: unknown, + result( + data: T, status?: number, headers?: Record ): void; - success(data?: unknown, headers?: Record): void; - failed( - data?: unknown, + success(data?: T, headers?: Record): void; + failed( + data?: T, code?: StatusCode, status?: number, headers?: Record @@ -118,21 +212,25 @@ interface ControllerInterface { log(...data: any): void; } +/** + * Base controller class providing response methods + * Implements standard response patterns for API endpoints + */ export declare class Controller implements ControllerInterface { - response( - data: unknown, + response( + data: T, code?: StatusCode, status?: number, headers?: Record ): void; - result( - data: unknown, + result( + data: T, status?: number, headers?: Record ): void; - success(data?: unknown, headers?: Record): void; - failed( - data?: unknown, + success(data?: T, headers?: Record): void; + failed( + data?: T, code?: StatusCode, status?: number, headers?: Record @@ -141,97 +239,428 @@ export declare class Controller implements ControllerInterface { log(...data: any): void; } +// ======================================== +// Validation Types +// ======================================== + +/** + * Configuration for request validation + */ interface ValidatorConfig { + /** Validation rules */ rules: Rules; + /** Custom error messages */ messages?: ErrorMessages; } +/** + * Validators for different parts of the request + */ interface RouterValidator { + /** Path parameter validation */ params?: ValidatorConfig; + /** Query parameter validation */ query?: ValidatorConfig; + /** Request body validation */ body?: ValidatorConfig; } -interface RouterInfo { +// ======================================== +// Router Types +// ======================================== + +/** + * Information about a matched route + * @template TParams Type of route parameters (defaults to Record) + * @template TBody Type of request body (defaults to any) + * @template TQuery Type of query parameters (defaults to any) + * + * @example + * ```typescript + * // RouterInfo is now framework-agnostic, using base AppContext + * interface UserParams { id: string; action: 'view' | 'edit'; } + * interface UserBody { name: string; email: string; } + * interface UserQuery { include?: 'profile'; } + * + * type UserRouterInfo = RouterInfo; + * + * // Can be used with any context type that extends AppContext + * const routerInfo: UserRouterInfo = { + * pathinfo: '/user/{:id}/{:action}', + * validators: {}, + * middlewares: [], // ContextHandler>[] + * handlers: [], // ContextHandler>[] + * afters: [], // ContextHandler>[] + * methods: ['POST', 'PUT'], + * params: { id: '123', action: 'edit' } + * }; + * ``` + */ +interface RouterInfo< + TParams = Record, + TBody = any, + TQuery = any +> { + /** Route path pattern */ pathinfo: string; + /** Route validators */ validators: RouterValidator; - middlewares: ContextHandler[]; - handlers: ContextHandler[]; - afters: ContextHandler[]; + /** Middleware functions */ + middlewares: ContextHandler>[]; + /** Handler functions */ + handlers: ContextHandler>[]; + /** After middleware functions */ + afters: ContextHandler>[]; + /** Supported HTTP methods */ methods: string[]; - params: { - [key: string]: string; - }; + /** Extracted path parameters */ + params: TParams; } -interface AppContext extends Context { - app: Application; - app_id: string; - method?: string; - pathinfo?: string; - params?: any; - config: AppConfiguration; - request_id: string; - router?: RouterInfo; - response?: HttpResponse | HttpError; -} +// ======================================== +// Context Types +// ======================================== +// ======================================== +// Server-Sent Events Types +// ======================================== + +/** + * Server-sent event data structure + */ interface IKoaSSEvent { + /** Event ID */ id?: number; + /** Event data */ data?: string | object; + /** Event type */ event?: string; } +/** + * Server-sent events interface extending Transform stream + */ interface IKoaSSE extends Transform { + /** Send SSE event */ send(data: IKoaSSEvent | string): void; + /** Send keep-alive ping */ keepAlive(): void; + /** Close SSE connection */ close(): void; } -interface KoaContext extends AppContext { +/** + * Base application context interface + * @template TParams Type of route parameters (defaults to Record) + * @template TBody Type of request body (defaults to any) + * @template TQuery Type of query parameters (defaults to any) + * + * @example + * ```typescript + * // Basic usage with default types + * interface MyContext extends AppContext {} + * + * // Usage with specific types + * interface UserParams { id: string; action: 'view' | 'edit'; } + * interface UserBody { name: string; email: string; } + * interface UserQuery { include?: 'profile' | 'settings'; } + * + * interface UserContext extends AppContext {} + * + * // In route handler + * const handler = async (context: UserContext) => { + * // context.router is now fully typed + * const routerParams = context.router.params; // UserParams + * const handlers = context.router.handlers; // ContextHandler>[] + * }; + * ``` + */ +interface AppContext< + TParams = Record, + TBody = any, + TQuery = any +> extends Context { + app: + | KoaApplication + | SocketApplication + | WebSocketApplication + | Application + | null; + app_id: string; + method: string; + pathinfo: string; + request_id?: string; + router?: RouterInfo | null; +} + +/** + * Koa-specific context extending AppContext + * @template TParams Type of route parameters (defaults to Record) + * @template TBody Type of request body (defaults to any) + * @template TQuery Type of query parameters (defaults to any) + * + * @example + * ```typescript + * // Define specific parameter and body types + * interface ProductParams { + * id: string; + * category: string; + * } + * + * interface CreateProductBody { + * name: string; + * price: number; + * description?: string; + * tags?: string[]; + * } + * + * interface ProductQuery { + * sort?: 'asc' | 'desc'; + * limit?: number; + * include?: 'details' | 'reviews' | 'images'; + * } + * + * // Create fully typed context + * type ProductContext = KoaContext; + * + * // Use in route handler with full type safety + * router.post('/product/{:id}/category/{:category}', async (context: ProductContext) => { + * // Full type safety for params + * const productId = context.params.id; // string + * const category = context.params.category; // string + * + * // Type-safe body access - TypeScript will enforce required fields + * const productName = context.body.name; // string + * const price = context.body.price; // number + * const desc = context.body.description; // string | undefined + * const tags = context.body.tags; // string[] | undefined + * + * // Type-safe query access + * const sortOrder = context.query.sort; // 'asc' | 'desc' | undefined + * const limit = context.query.limit; // number | undefined + * const include = context.query.include; // 'details' | 'reviews' | 'images' | undefined + * + * // TypeScript will catch type errors at compile time + * // const invalid = context.body.invalidField; // ❌ TypeScript error + * // const wrongType = context.query.sort === 'invalid'; // ❌ TypeScript error + * }); + * + * // Partial typing - only specify what you need + * type SimpleContext = KoaContext<{}, CreateProductBody>; // Only body typed + * type ParamsOnlyContext = KoaContext; // Only params typed + * type QueryOnlyContext = KoaContext<{}, any, ProductQuery>; // Only query typed + * + * // Real-world example: User management API + * interface UserParams { id: string; } + * interface UpdateUserBody { + * name?: string; + * email?: string; + * role?: 'admin' | 'user'; + * } + * interface UserQuery { + * expand?: 'profile' | 'permissions'; + * format?: 'json' | 'xml'; + * } + * + * const userRouter = new Router>(); + * + * userRouter.put('/user/{:id}', async (context) => { + * // All properties are fully typed with IntelliSense support + * const userId = context.params.id; + * const updates = context.body; // UpdateUserBody + * const options = context.query; // UserQuery + * + * // Type-safe validation + * if (updates.role && !['admin', 'user'].includes(updates.role)) { + * // This would be caught at compile time due to literal types + * } + * }); + * ``` + */ +interface KoaContext< + TParams = Record, + TBody = any, + TQuery = any +> extends AppContext { + /** Route parameters */ + params?: TParams; + /** Application configuration */ + config?: AppConfiguration; + /** Koa context with optional SSE support */ koa: Koa.ParameterizedContext & { sse?: IKoaSSE }; - method: HttpMethod; + /** Request URL */ url: string; - // eslint-disable-next-line no-use-before-define - router?: RouterInfo | null; - access_key_id?: string; - app_key?: string; - body?: any; + /** Request body */ + body?: TBody; + /** Uploaded file */ file?: File | null; + /** Uploaded files array */ files?: File[]; - query?: any; + /** Query parameters */ + query?: TQuery; + /** Request headers */ headers?: IncomingHttpHeaders; + /** Response object */ response?: HttpResponse | HttpError; } -type ContextHandler = (context: T) => Promise; +/** + * Context handler function type + * @template T Context type extending AppContext + */ +type ContextHandler = AppContext> = ( + context: T +) => Promise; + +// ======================================== +// Router Class +// ======================================== -interface RouterOptions { +/** + * Router options for configuration + * @template T Context type extending AppContext + * + * @example + * ```typescript + * // RouterOptions now uses base AppContext, making it framework-agnostic + * interface UserParams { id: string; } + * interface UserBody { name: string; } + * interface UserQuery { format?: 'json' | 'xml'; } + * + * type UserContext = AppContext; + * + * const routerOptions: RouterOptions = { + * method: 'POST', + * middlewares: [ + * async (context) => { + * // context is typed as UserContext + * console.log(`Processing user ${context.params?.id}`); + * } + * ], + * handlers: [ + * async (context) => { + * // Full type safety + * const userId = context.params?.id; // string | undefined + * const userName = context.body?.name; // string | undefined + * const format = context.query?.format; // 'json' | 'xml' | undefined + * } + * ] + * }; + * ``` + */ +interface RouterOptions = AppContext> { + /** Default HTTP method */ method?: HttpMethod; + /** Route handlers */ handlers?: ContextHandler[]; + /** Middleware functions */ middlewares?: ContextHandler[]; + /** After middleware functions */ afters?: ContextHandler[]; + /** Route description */ intro?: string; - routers?: Router[]; + /** Sub-routers */ + routers?: Router[]; + /** Route validators */ validators?: RouterValidator; } -export class Router { +/** + * Router class for defining API routes and middleware + * @template T Context type extending AppContext (can be KoaContext, SocketContext, etc.) + * + * @example + * ```typescript + * // Basic router usage + * const router = new Router(); + * + * // Router with typed params, body, and query using AppContext + * interface UserParams { + * id: string; + * action: 'view' | 'edit' | 'delete'; + * } + * + * interface UpdateUserBody { + * name?: string; + * email?: string; + * age?: number; + * } + * + * interface UserQuery { + * include?: 'profile' | 'settings'; + * format?: 'json' | 'xml'; + * } + * + * // Can use base AppContext + * type UserContext = AppContext; + * const userRouter = new Router(); + * + * // Or specific implementations like KoaContext + * type KoaUserContext = KoaContext; + * const koaRouter = new Router(); + * + * // Or SocketContext + * type SocketUserContext = SocketContext; + * const socketRouter = new Router(); + * + * userRouter.post('/user/{:id}/{:action}', async (context) => { + * // context.params is fully typed + * const userId = context.params?.id; // string | undefined + * const action = context.params?.action; // 'view' | 'edit' | 'delete' | undefined + * + * // Router info is also typed + * if (context.router) { + * const routerParams = context.router.params; // UserParams + * const handlers = context.router.handlers; // ContextHandler[] + * } + * }); + * + * // Router with middleware that uses typed context + * const apiRouter = new Router(null, { + * middlewares: [ + * async (context) => { + * console.log(`User ${context.params?.id} performing ${context.params?.action}`); + * console.log(`App: ${context.app?.constructor.name}`); + * } + * ] + * }); + * ``` + */ +export class Router = AppContext> { + /** Route prefix */ prefix: string; + /** Default HTTP method */ method: HttpMethod; - routers: Router[]; + /** Sub-routers */ + routers: Router[]; + /** Route handlers */ handlers: ContextHandler[]; + /** Middleware functions */ middlewares: ContextHandler[]; + /** Route validators */ validators: RouterValidator; + /** After middleware functions */ afters?: ContextHandler[]; constructor(prefix?: string, options?: RouterOptions); - add(...router: Router[]): this; - add(prefix: string, ...router: Router[]): this; + /** + * Add sub-routers to this router + */ + add>(...router: Router[]): this; + add>( + prefix: string, + ...router: Router[] + ): this; + /** + * Create a new sub-router + */ new(prefix: string, options?: RouterOptions): this; + /** + * Add a route with specific HTTP method + */ push( method: HttpMethod, prefix: string, @@ -239,36 +668,54 @@ export class Router { validator?: RouterValidator ): this; + /** + * Add a GET route + */ get( prefix: string, handle: ContextHandler, validator?: RouterValidator ): this; + /** + * Add a POST route + */ post( prefix: string, handle: ContextHandler, validator?: RouterValidator ): this; + /** + * Add a PUT route + */ put( prefix: string, handle: ContextHandler, validator?: RouterValidator ): this; + /** + * Add a PATCH route + */ patch( prefix: string, handle: ContextHandler, validator?: RouterValidator ): this; + /** + * Add a DELETE route + */ delete( prefix: string, handle: ContextHandler, validator?: RouterValidator ): this; + /** + * Add a route that accepts any HTTP method + */ any( prefix: string, handle: ContextHandler, @@ -276,118 +723,539 @@ export class Router { ): this; } +// ======================================== +// SSE Middleware Types +// ======================================== + +/** + * Options for Server-Sent Events middleware + */ type SSEOptions = { - pingInterval?: number; // default is 60000 - closeEvent?: string; // default is 'close' + /** Ping interval in milliseconds (default: 60000) */ + pingInterval?: number; + /** Event name for close event (default: 'close') */ + closeEvent?: string; }; -interface AppConfiguration { - [key: string]: any; - debug?: boolean; - app_id?: string; - routers?: Router[]; -} - +/** + * SSE context handler function type + */ type SSEContextHandler = ( context: Koa.ParameterizedContext, next: () => Promise ) => Promise; +/** + * Middleware namespace containing utility middleware functions + */ export namespace middlewares { + /** + * Create Server-Sent Events middleware + * @param options SSE configuration options + * @returns SSE middleware function + */ function KoaSSEMiddleware(options?: SSEOptions): SSEContextHandler; } +// ======================================== +// Application Configuration Types +// ======================================== + +/** + * Base application configuration interface + * + * @example + * ```typescript + * // Basic usage - supports mixed router types + * const config: AppConfiguration = { + * debug: true, + * app_id: 'my-app', + * routers: [ + * userRouter, // Router> + * productRouter, // Router> + * apiRouter // Router> + * ] + * }; + * + * // Type-safe usage with helper type + * const typedConfig: TypedAppConfiguration< + * Router>[] + * > = { + * debug: true, + * routers: [userRouter] // All routers must be of the same type + * }; + * ``` + */ +interface AppConfiguration { + [key: string]: any; + /** Enable debug mode */ + debug?: boolean; + /** Application identifier */ + app_id?: string; + /** Application routers - supports mixed router types for flexibility */ + routers?: Router[]; +} + +/** + * Typed application configuration for strict type checking when needed + * @template TRouters Array type of routers for strict typing + * + * @example + * ```typescript + * interface UserParams { id: string; } + * interface UserBody { name: string; } + * interface UserQuery { format?: 'json' | 'xml'; } + * + * type UserRouter = Router>; + * + * // Strict typing when all routers are of the same type + * const config: TypedAppConfiguration = { + * debug: true, + * routers: [ + * userRouter1, // Must be UserRouter + * userRouter2 // Must be UserRouter + * ] + * }; + * + * // Or for multiple specific types + * const mixedConfig: TypedAppConfiguration<(UserRouter | ProductRouter)[]> = { + * routers: [userRouter, productRouter] + * }; + * ``` + */ +interface TypedAppConfiguration[] = Router[]> + extends Omit { + /** Strictly typed application routers */ + routers?: TRouters; +} + +/** + * Koa application specific configuration + * + * @example + * ```typescript + * // Basic usage with mixed router types + * const config: KoaApplicationConfig = { + * listen_host: 'localhost', + * port: 3000, + * debug: true, + * routers: [ + * userRouter, // Different context types + * productRouter, // are supported + * apiRouter + * ] + * }; + * + * // Type-safe usage for specific router types + * interface UserParams { id: string; } + * interface UserBody { name: string; } + * type UserRouter = Router>; + * + * const typedConfig: TypedKoaApplicationConfig = { + * listen_host: 'localhost', + * port: 3000, + * routers: [userRouter1, userRouter2] // All must be UserRouter + * }; + * ``` + */ export type KoaApplicationConfig = AppConfiguration & { + /** Host to listen on */ listen_host: string; + /** Number of server instances */ count?: number; + /** Port to listen on */ port?: number; + /** Path mappings */ paths?: Record; + /** Koa server configuration */ server?: { + /** Environment mode */ env?: string | undefined; + /** Signing keys for cookies */ keys?: string[] | undefined; + /** Trust proxy headers */ proxy?: boolean | undefined; + /** Subdomain offset */ subdomainOffset?: number | undefined; + /** Proxy IP header */ proxyIpHeader?: string | undefined; + /** Maximum IPs count */ maxIpsCount?: number | undefined; }; + /** Session key name */ session_key?: string; + /** Session configuration */ session?: Partial; + /** Static file serving options */ static?: KoaStaticServer.Options; }; +/** + * Typed Koa application configuration for strict type checking + * @template TRouters Array type of routers for strict typing + */ +export type TypedKoaApplicationConfig< + TRouters extends Router[] = Router[] +> = TypedAppConfiguration & + Omit; + +/** + * Socket application configuration + * + * @example + * ```typescript + * // Basic usage with mixed router types + * const config: SocketAppConfiguration = { + * port: 8080, + * debug: true, + * routers: [ + * chatRouter, // Different context types + * gameRouter, // are supported + * notifyRouter + * ], + * ping: { open: true, interval: 30000 } + * }; + * + * // Type-safe usage for specific router types + * interface ChatParams { room: string; userId: string; } + * interface ChatBody { message: string; type: 'text' | 'image'; } + * type ChatRouter = Router>; + * + * const typedConfig: TypedSocketAppConfiguration = { + * port: 8080, + * routers: [chatRouter1, chatRouter2] // All must be ChatRouter + * }; + * ``` + */ +export type SocketAppConfiguration = AppConfiguration & { + /** Port to listen on */ + port: number; + /** Ping configuration */ + ping?: { + /** Enable ping */ + open?: boolean; + /** Ping interval in milliseconds */ + interval?: number; + /** Ping data */ + data?: any; + }; +}; + +/** + * Typed Socket application configuration for strict type checking + * @template TRouters Array type of routers for strict typing + */ +export type TypedSocketAppConfiguration< + TRouters extends Router[] = Router[] +> = TypedAppConfiguration & + Omit; + +/** + * WebSocket application configuration + * + * @example + * ```typescript + * // Basic usage with mixed router types + * const config: WebSocketAppConfiguration = { + * port: 8080, + * debug: true, + * routers: [ + * wsRouter1, // Different context types + * wsRouter2, // are supported + * wsRouter3 + * ], + * // WebSocket server options + * clientTracking: true, + * maxPayload: 1024 * 1024 + * }; + * + * // Type-safe usage for specific router types + * interface WSParams { channel: string; userId: string; } + * interface WSBody { event: string; data: any; } + * type WSRouter = Router>; + * + * const typedConfig: TypedWebSocketAppConfiguration = { + * port: 8080, + * routers: [wsRouter1, wsRouter2] // All must be WSRouter + * }; + * ``` + */ +export type WebSocketAppConfiguration = ServerOptions & SocketAppConfiguration; + +/** + * Typed WebSocket application configuration for strict type checking + * @template TRouters Array type of routers for strict typing + */ +export type TypedWebSocketAppConfiguration< + TRouters extends Router[] = Router[] +> = ServerOptions & TypedSocketAppConfiguration; + +// ======================================== +// Application Classes +// ======================================== + +/** + * Trigger function type for events + */ type TriggerFunc = (...args: any[]) => void; +/** + * Base application class extending EventEmitter + */ export declare abstract class Application extends EventEmitter { + /** Application routes */ routes: any; + /** Application identifier */ app_id: string; + /** Application configuration */ config: Configuration; + constructor(config: AppConfiguration); + + /** + * Start the application + * @returns Promise that resolves when application is started + */ abstract start(): Promise; } +/** + * Koa-based HTTP application + */ export declare class KoaApplication extends Application { + /** Koa instance */ koa: Koa; + /** Workflow instance for request processing */ workflow: Workflow; + constructor(config: KoaApplicationConfig); + + /** + * Start the Koa application server + * @returns Promise that resolves when server is started + */ start(): Promise; } -export interface SocketContext extends AppContext { +/** + * Socket context extending AppContext + * @template TParams Type of route parameters (defaults to Record) + * @template TBody Type of request body (defaults to any) + * @template TQuery Type of query parameters (defaults to any) + * + * @example + * ```typescript + * // Define socket-specific types + * interface SocketParams { room: string; userId: string; } + * interface SocketBody { message: string; type: 'text' | 'image'; } + * interface SocketQuery { token?: string; } + * + * type ChatContext = SocketContext; + * + * // Use in socket handler + * const socketHandler = async (context: ChatContext) => { + * // All properties are fully typed + * const room = context.params.room; // string + * const userId = context.params.userId; // string + * const message = context.body.message; // string + * const msgType = context.body.type; // 'text' | 'image' + * const token = context.query.token; // string | undefined + * + * // Router info is also typed + * const routerParams = context.router?.params; // SocketParams + * }; + * ``` + */ +export interface SocketContext< + TParams = Record, + TBody = any, + TQuery = any +> extends AppContext { + /** Route parameters */ + params?: TParams; + /** Application configuration */ + config?: AppConfiguration; + /** Socket connection */ socket: Socket; + /** Request body */ + body?: TBody; + /** Query parameters */ + query?: TQuery; + /** Request headers */ + headers?: IncomingHttpHeaders; + /** Response object */ + response?: HttpResponse | HttpError; +} + +/** + * Socket client wrapper + */ +export declare class SocketClient { + /** Socket connection options */ + options: { + /** Port number */ + port: number; + /** Host address */ + host: string; + /** Client name */ + name?: string; + }; + /** Event emitter for socket events */ + event: EventEmitter; + /** Socket client instance */ + client: Socket; + + constructor(socket: Socket, app_id: string); + + /** + * Send data to socket client + * @param data Data to send + */ + send(data: any): void; + + /** + * Close socket connection + */ + close(): void; +} + +/** + * Socket-based application + */ +export declare class SocketApplication extends Application { + constructor(config: SocketAppConfiguration); + + /** + * Start the socket application server + * @returns Promise that resolves when server is started + */ + start(): Promise; + + /** + * Broadcast data to all connected clients + * @param data Data to broadcast + * @param msg Message + * @param code Status code + * @param connections Specific connections to broadcast to + */ + broadcast( + data?: any, + msg?: string, + code?: number, + connections?: Socket[] + ): void; +} + +/** + * WebSocket-based application + */ +export declare class WebSocketApplication extends Application { + constructor(config: WebSocketAppConfiguration); + + /** + * Start the WebSocket application server + * @returns Promise that resolves when server is started + */ + start(): Promise; + + /** + * Broadcast data to all connected WebSocket clients + * @param data Data to broadcast + * @param msg Message + * @param code Status code + * @param connections Specific connections to broadcast to + */ + broadcast( + data?: any, + msg?: string, + code?: number, + connections?: WebSocket[] + ): void; } +// ======================================== +// Model Class +// ======================================== + +/** + * Base model class for data validation and manipulation + */ export declare class Model { constructor(obj?: { [key: string]: any }, rules?: Rules, msg?: ErrorMessages); + /** + * Create a new model instance + * @param obj Initial data object + * @param rules Validation rules + * @param msg Custom error messages + * @returns New model instance + */ static create( obj?: { [key: string]: any }, rules?: Rules, msg?: ErrorMessages ): T; + /** + * Convert model to JSON string + * @returns JSON string representation + */ toJson(): string; + /** + * Get all property names + * @returns Array of property names + */ properties(): Array; + /** + * Get property count + * @returns Number of properties + */ count(): number; + /** + * Validate model data + * @param rules Validation rules + * @param msg Custom error messages + * @returns Validator instance + */ validate(rules: Rules, msg?: ErrorMessages): Validator; } +// ======================================== +// Utility Functions +// ======================================== + +/** + * Initialize application context + * @template T Application type + * @template F Context type extending AppContext + * @template TParams Type of route parameters + * @template TBody Type of request body + * @template TQuery Type of query parameters + * @param options Context initialization options + * @returns Initialized context + */ export function initContext< T extends Application, - F extends AppContext + F extends AppContext = AppContext< + TParams, + TBody, + TQuery + >, + TParams = Record, + TBody = any, + TQuery = any >(options: { + /** Application instance */ app: T; + /** Application routes */ routes: Router[]; + /** HTTP method */ method?: string; + /** Request path */ pathinfo?: string; + /** Application ID */ app_id?: string; }): F & { app: T }; - -export declare class SocketClient { - options: { - port: number; - host: string; - name?: string; - }; - event: EventEmitter; - client: Socket; - constructor(socket: Socket, app_id: string); - send(data: any): void; - close(): void; -} - -export type SocketAppConfiguration = AppConfiguration & { - port: number; - ping?: { - open?: boolean; - interval?: number; - data?: any; - }; -}; - -export declare class SocketApplication extends Application { - constructor(config: SocketAppConfiguration); - start(): Promise; -} From 45c22a2e3b1cf3376cb1a575c95855d88540b7dc Mon Sep 17 00:00:00 2001 From: axiosleo Date: Tue, 8 Jul 2025 09:40:56 +0800 Subject: [PATCH 18/18] fix: correct initContext function type clarity --- index.d.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.d.ts b/index.d.ts index 58882d5..1e9e959 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1230,23 +1230,23 @@ export declare class Model { /** * Initialize application context * @template T Application type - * @template F Context type extending AppContext * @template TParams Type of route parameters * @template TBody Type of request body * @template TQuery Type of query parameters + * @template F Context type extending AppContext * @param options Context initialization options * @returns Initialized context */ export function initContext< T extends Application, + TParams = Record, + TBody = any, + TQuery = any, F extends AppContext = AppContext< TParams, TBody, TQuery - >, - TParams = Record, - TBody = any, - TQuery = any + > >(options: { /** Application instance */ app: T;