Compare commits

...

16 Commits
v0.0.3 ... main

Author SHA1 Message Date
ee7007f050 修正函数返回值 2025-12-12 21:32:26 +08:00
627c05fd06 升级异常处理机制 2025-12-12 21:20:57 +08:00
fa97cace29 export interface; -w 时自动监听没有content-type的html 2025-12-10 16:56:37 +08:00
069a4b95c2 use DontStartLogAuto to disable log auto stop in ssgo/s 2025-12-03 11:02:15 +08:00
e64727d2ef update for gojs 2025-12-01 00:37:09 +08:00
baa4d430cc update gojs 2025-11-30 23:11:11 +08:00
57db518ef9 update for gojs 2025-07-28 22:56:48 +08:00
715de5e442 使用 gojs/task 代替内置任务
支持 load 加载的服务热更新(需配置hotLoad)
增强唯一id获取(使用新算法,依赖Redis)
session支持基于细粒度权限匹配(传入支持的功能列表匹配)
2025-07-24 21:44:41 +08:00
afcce4d649 删除调试信息 2025-05-02 09:13:52 +08:00
38cf9933be 修复watch时重启服务bug(重制configed = false以重新初始化配置) 2025-05-02 09:09:59 +08:00
Star
a1f5f72181 change default userIdKey to "id" 2025-01-14 14:32:24 +08:00
b1796dad4d fix bug for sessionWithoutCookie 2024-12-15 19:51:43 +08:00
e9d4ec6bc7 add config sessionWithoutCookie & deviceWithoutCookie 2024-12-15 19:32:27 +08:00
5352152b66 fix args error for DownloadFile
fix bug for watch
2024-12-13 19:37:46 +08:00
93cfbde981 fix bug from ssgo/u 2024-11-30 10:53:25 +08:00
7bb6938cb7 update id
add tpl
support reload on watched file changes
2024-11-30 10:44:02 +08:00
22 changed files with 1442 additions and 676 deletions

2
.gitignore vendored
View File

@ -2,8 +2,6 @@
!.gitignore !.gitignore
go.sum go.sum
/build /build
/node_modules
/package.json
/bak /bak
node_modules node_modules
package.json package.json

View File

@ -22,7 +22,10 @@ func NewCaller(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
func makeResult(r *httpclient.Result, vm *goja.Runtime) goja.Value { func makeResult(r *httpclient.Result, vm *goja.Runtime) goja.Value {
if r.Error != nil { if r.Error != nil {
panic(vm.NewGoError(r.Error)) // panic(vm.NewGoError(r.Error))
vm.SetData("_lastError", r.Error)
gojs.GetLogger(vm).Error(r.Error.Error())
return nil
} }
headers := map[string]string{} headers := map[string]string{}
for k, v := range r.Response.Header { for k, v := range r.Response.Header {

56
go.mod
View File

@ -1,40 +1,54 @@
module apigo.cc/gojs/service module apigo.cc/gojs/service
go 1.18 go 1.24.0
require ( require (
apigo.cc/gojs v0.0.4 apigo.cc/gojs v0.0.32
apigo.cc/gojs/console v0.0.1 apigo.cc/gojs/console v0.0.4
apigo.cc/gojs/http v0.0.3 apigo.cc/gojs/file v0.0.7
apigo.cc/gojs/util v0.0.3 apigo.cc/gojs/http v0.0.8
apigo.cc/gojs/runtime v0.0.4
apigo.cc/gojs/task v0.0.8
apigo.cc/gojs/util v0.0.16
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/ssgo/config v1.7.8 github.com/ssgo/config v1.7.10
github.com/ssgo/discover v1.7.9 github.com/ssgo/discover v1.7.10
github.com/ssgo/httpclient v1.7.8 github.com/ssgo/httpclient v1.7.8
github.com/ssgo/log v1.7.7 github.com/ssgo/log v1.7.10
github.com/ssgo/redis v1.7.7 github.com/ssgo/redis v1.7.8
github.com/ssgo/s v1.7.18 github.com/ssgo/s v1.7.25
github.com/ssgo/standard v1.7.7 github.com/ssgo/standard v1.7.7
github.com/ssgo/u v1.7.9 github.com/ssgo/u v1.7.23
) )
require ( require (
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/ZZMarquis/gm v1.3.2 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emmansun/gmsm v0.40.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-rod/rod v0.116.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/gomodule/redigo v1.9.2 // indirect github.com/gomodule/redigo v1.9.2 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/google/pprof v0.0.0-20250903194437-c28834ac2320 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/ssgo/tool v0.4.27 // indirect github.com/ssgo/tool v0.4.29 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect github.com/tklauser/numcpus v0.10.0 // indirect
github.com/ysmood/fetchup v0.2.3 // indirect
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/got v0.40.0 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/net v0.30.0 // indirect golang.org/x/crypto v0.46.0 // indirect
golang.org/x/sys v0.26.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/text v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@ -91,7 +91,10 @@ func (r *Request) MakeURL(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value
func (r *Request) ReadAll(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { func (r *Request) ReadAll(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
data, err := io.ReadAll(r.req.Body) data, err := io.ReadAll(r.req.Body)
if err != nil { if err != nil {
panic(vm.NewGoError(err)) // panic(vm.NewGoError(err))
vm.SetData("_lastError", err)
gojs.GetLogger(vm).Error(err.Error())
return nil
} }
return vm.ToValue(data) return vm.ToValue(data)
} }
@ -102,7 +105,10 @@ func (r *Request) Read(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
data := make([]byte, size) data := make([]byte, size)
n, err := r.req.Body.Read(data) n, err := r.req.Body.Read(data)
if err != nil { if err != nil {
panic(vm.NewGoError(err)) // panic(vm.NewGoError(err))
vm.SetData("_lastError", err)
gojs.GetLogger(vm).Error(err.Error())
return nil
} }
return vm.ToValue(data[0:n]) return vm.ToValue(data[0:n])
} }
@ -110,7 +116,10 @@ func (r *Request) Read(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
func (r *Request) Close(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { func (r *Request) Close(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
err := r.req.Body.Close() err := r.req.Body.Close()
if err != nil { if err != nil {
panic(vm.NewGoError(err)) // panic(vm.NewGoError(err))
vm.SetData("_lastError", err)
gojs.GetLogger(vm).Error(err.Error())
return nil
} }
return nil return nil
} }

View File

@ -93,7 +93,10 @@ func (r *Response) Write(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value
args := gojs.MakeArgs(&argsIn, vm).Check(1) args := gojs.MakeArgs(&argsIn, vm).Check(1)
n, err := r.resp.Write(args.Bytes(0)) n, err := r.resp.Write(args.Bytes(0))
if err != nil { if err != nil {
panic(vm.NewGoError(err)) // panic(vm.NewGoError(err))
vm.SetData("_lastError", err)
gojs.GetLogger(vm).Error(err.Error())
return vm.ToValue(false)
} }
return vm.ToValue(n) return vm.ToValue(n)
} }
@ -111,7 +114,7 @@ func (r *Response) SendFile(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Val
func (r *Response) DownloadFile(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { func (r *Response) DownloadFile(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(3) args := gojs.MakeArgs(&argsIn, vm).Check(3)
r.resp.DownloadFile(args.Str(0), args.Str(1), args.Bytes(1)) r.resp.DownloadFile(args.Str(0), args.Str(1), args.Bytes(2))
return nil return nil
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,30 @@
// just for develop // just for develop
export default { export default {
config, config,
start, start,
stop, stop,
register, register,
load, load,
task, // task,
newCaller, newCaller,
dataSet, // dataSet,
dataGet, // dataGet,
dataKeys, // dataKeys,
dataCount, // dataCount,
dataFetch, // dataFetch,
dataRemove, // dataRemove,
listPush, // listPush,
listPop, // listPop,
listCount, // listCount,
listRemove, // listRemove,
id,
// idL,
// uniqueId,
// uniqueIdL,
// setTplFunc,
setTplReplaces,
tpl,
} }
function config(config?: Config): void { } function config(config?: Config): void { }
@ -26,251 +33,264 @@ function stop(): void { }
function register(option: RegisterOption, callback: (params: RequestParams) => void): any { return null } function register(option: RegisterOption, callback: (params: RequestParams) => void): any { return null }
function load(serviceFile: string, poolConfig?: PoolConfig): void { } function load(serviceFile: string, poolConfig?: PoolConfig): void { }
function task(taskFile: string, interval: number = 1000): void { } // function task(taskFile: string, interval: number = 1000): void { }
function newCaller(): Caller { return null as any } function newCaller(): Caller { return null as any }
function dataSet(scope: string, key: string, value: any): void { } // function dataSet(scope: string, key: string, value: any): void { }
function dataGet(scope: string, key: string): any { return null } // function dataGet(scope: string, key: string): any { return null }
function dataKeys(scope: string): string[] { return [] } // function dataKeys(scope: string): string[] { return [] }
function dataCount(scope: string): number { return 0 } // function dataCount(scope: string): number { return 0 }
function dataFetch(scope: string): Map<string, any> { return null as any } // function dataFetch(scope: string): Map<string, any> { return null as any }
function dataRemove(scope: string, key?: string): void { } // function dataRemove(scope: string, key?: string): void { }
function listPush(scope: string, key: string, value: any): void { } // function listPush(scope: string, key: string, value: any): void { }
function listPop(scope: string, key: string): any { return null } // function listPop(scope: string, key: string): any { return null }
function listCount(scope: string): number { return 0 } // function listCount(scope: string): number { return 0 }
function listRemove(scope: string): void { } // function listRemove(scope: string): void { }
function id(size?: number): string { return '' }
// function id(space: string, size?: number): string { return '' }
// function idL(space: string, size?: number): string { return '' }
// function uniqueId(size?: number): string { return '' }
// function uniqueIdL(size?: number): string { return '' }
// function setTplFunc(fnList: Object): void { }
function setTplReplaces(replaces: Object): void { }
function tpl(file: string, data: Object, fnList?: Object): string { return '' }
export interface Config {
// github.com/ssgo/s 的配置参数
listen: string // 监听端口(|隔开多个监听)(,隔开多个选项如果不指定IP则监听在0.0.0.0如果不指定端口则使用h2c协议监听在随机端口80端口默认使用http协议443端口默认使用https协议例如 80,http|443|443:h2|127.0.0.1:8080,h2c
ssl: Map<string, CertSet> // SSL证书配置key为域名value为cert和key的文件路径
noLogGets: boolean // 不记录GET请求的日志
noLogHeaders: string // 不记录请求头中包含的这些字段多个字段用逗号分隔默认不记录Accept,Accept-Encoding,Cache-Control,Pragma,Connection,Upgrade-Insecure-Requests
logInputArrayNum: number // 请求字段中容器类型数组、Map在日志打印个数限制 默认为10个多余的数据将不再日志中记录
logInputFieldSize: number // 请求字段中单个字段在日志打印长度限制 默认为500个字符多余的数据将不再日志中记录
noLogOutputFields: string // 不记录响应字段中包含的这些字段key名多个字段用逗号分隔
logOutputArrayNum: number // 响应字段中容器类型数组、Map在日志打印个数限制 默认为3个多余的数据将不再日志中记录
logOutputFieldSize: number // 响应字段中单个字段在日志打印长度限制 默认为100个字符多余的数据将不再日志中记录
logWebsocketAction: boolean // 记录Websocket中每个Action的请求日志默认不记录
compress: boolean // 是否启用压缩,默认不启用
compressMinSize: number // 小于设定值的应答内容将不进行压缩默认值1024
compressMaxSize: number // 大于设定值的应答内容将不进行压缩默认值4096000
checkDomain: string // 心跳检测时使用域名默认使用IP地址心跳检测使用 HEAD /__CHECK__ 请求,应答 299 表示正常593 表示异常
redirectTimeout: number // proxy和discover发起请求时的超时时间单位ms默认值10000
acceptXRealIpWithoutRequestId: boolean // 是否允许头部没有携带请求ID的X-Real-IP信息默认不允许防止伪造客户端IP
statisticTime: boolean // 是否开启请求时间统计,默认不开启
statisticTimeInterval: number // 统计时间间隔单位ms默认值10000
fast: boolean // 是否启用快速模式(为了追求性能牺牲一部分特性),默认不启用
maxUploadSize: number // 最大上传文件大小multipart/form-data请求的总空间单位字节默认值104857600
cpu: number // CPU占用的核数默认为0即不做限制
memory: number // 内存单位M默认为0即不做限制
cpuMonitor: boolean // 在日志中记录CPU使用情况默认不开启
memoryMonitor: boolean // 在日志中记录内存使用情况,默认不开启
cpuLimitValue: number // CPU超过最高占用值10-100超过次数将自动重启如果CpuMonitor开启的话默认100
memoryLimitValue: number // 内存超过最高占用值10-100超过次数将自动重启如果MemoryMonitor开启的话默认95
cpuLimitTimes: number // CPU超过最高占用值超过次数1-100将报警如果CpuMonitor开启的话默认6即30秒内连续6次
memoryLimitTimes: number // 内存超过最高占用值超过次数1-100将报警如果MemoryMonitor开启的话默认6即30秒内连续6次
cookieScope: string // 启用Session时Cookie的有效范围host|domain|topDomain默认值为host
sessionWithoutCookie: boolean // Session禁用Cookie保持默认使用Cookie
deviceWithoutCookie: boolean // 设备ID禁用Cookie保持默认使用Cookie
idServer: string // 用s.UniqueId、s.Id来生成唯一ID雪花算法时所需的redis服务器连接如果不指定将不能实现跨服务的全局唯一
keepKeyCase: boolean // 是否保持Key的首字母大小写默认保持设置为false则自动将首字母转为小写
indexFiles: string[] // 访问静态文件时的索引文件,默认为 index.html
indexDir: boolean // 访问目录时显示文件列表
readTimeout: number // 读取请求的超时时间单位ms
readHeaderTimeout: number // 读取请求头的超时时间单位ms
writeTimeout: number // 响应写入的超时时间单位ms
idleTimeout: number // 连接空闲超时时间单位ms
maxHeaderBytes: number // 请求头的最大字节数
maxHandlers: number // 每个连接的最大处理程序数量
maxConcurrentStreams: number // 每个连接的最大并发流数量
maxDecoderHeaderTableSize: number // 解码器头表的最大大小
maxEncoderHeaderTableSize: number // 编码器头表的最大大小
maxReadFrameSize: number // 单个帧的最大读取大小
maxUploadBufferPerConnection: number // 每个连接的最大上传缓冲区大小
maxUploadBufferPerStream: number // 每个流的最大上传缓冲区大小
interface Config { // 其他配置
// github.com/ssgo/s 的配置参数 sessionKey: string // HTTP头和Cookie中SessionID的Key客户端没有传递时服务端自动生成Header的优先级高于Cookie默认为 Session, 设置为空时表示不使用
listen: string // 监听端口(|隔开多个监听)(,隔开多个选项如果不指定IP则监听在0.0.0.0如果不指定端口则使用h2c协议监听在随机端口80端口默认使用http协议443端口默认使用https协议例如 80,http|443|443:h2|127.0.0.1:8080,h2c deviceKey: string // 标识设备ID的Key客户端没有传递时服务端自动生成Header的优先级高于Cookie默认为 Device 设置为空时表示不使用
ssl: Map<string, CertSet> // SSL证书配置key为域名value为cert和key的文件路径 clientKey: string // 标识客户端的Key默认为 Client对应的Header头为 ClientName 和 ClientVersion
noLogGets: boolean // 不记录GET请求的日志 userIdKey: string // session中userID的Key用于在日志中记录用户ID信息默认为 id
noLogHeaders: string // 不记录请求头中包含的这些字段多个字段用逗号分隔默认不记录Accept,Accept-Encoding,Cache-Control,Pragma,Connection,Upgrade-Insecure-Requests sessionProvider: string // 指定一个redis连接例如redis://:sskey加密的密码@127.0.0.1:6379/15默认使用内存存储
logInputArrayNum: number // 请求字段中容器类型数组、Map在日志打印个数限制 默认为10个多余的数据将不再日志中记录 sessionTimeout: number // session过期时间单位 秒,默认为 3600秒
logInputFieldSize: number // 请求字段中单个字段在日志打印长度限制 默认为500个字符多余的数据将不再日志中记录 authFieldMessage: string | Object // 身份验证失败时的消息,默认为 auth failed可以设置对象来返回JSON可以使用模版 {{TARGET_AUTHLEVEL}}、{{USER_AUTHLEVEL}}
noLogOutputFields: string // 不记录响应字段中包含的这些字段key名多个字段用逗号分隔 verifyFieldMessage: string | Object // 参数验证失败时的消息,默认为 verify failed可以设置对象来返回JSON可以使用模版 {{FAILED_FIELDS}}
logOutputArrayNum: number // 响应字段中容器类型数组、Map在日志打印个数限制 默认为3个多余的数据将不再日志中记录 limitedMessage: string | Object // 访问受限时的消息,默认为 too many requests可以设置对象来返回JSON可以使用模版 {{LIMITED_FROM}}、{{LIMITED_VALUE}}
logOutputFieldSize: number // 响应字段中单个字段在日志打印长度限制 默认为100个字符多余的数据将不再日志中记录 limiterRedis: string // 限流器使用的Redis连接默认使用内存存储
logWebsocketAction: boolean // 记录Websocket中每个Action的请求日志默认不记录 limiters: Map<string, LimiterConfig> // 限流器配置from 为数据来源例如ip、user、device、header.User-Agent、in.phone 等in表示从请求参数中获取time为时间间隔单位mstimes为 时间间隔内允许访问的次数
compress: boolean // 是否启用压缩,默认不启用 hotLoad: number // 热加载配置单位s默认值00表示不检测热加载
compressMinSize: number // 小于设定值的应答内容将不进行压缩默认值1024 tplSafePaths: string[] // 模板文件的安全路径,默认不开启安全检查
compressMaxSize: number // 大于设定值的应答内容将不进行压缩默认值4096000
checkDomain: string // 心跳检测时使用域名默认使用IP地址心跳检测使用 HEAD /__CHECK__ 请求,应答 299 表示正常593 表示异常
redirectTimeout: number // proxy和discover发起请求时的超时时间单位ms默认值10000
acceptXRealIpWithoutRequestId: boolean // 是否允许头部没有携带请求ID的X-Real-IP信息默认不允许防止伪造客户端IP
statisticTime: boolean // 是否开启请求时间统计,默认不开启
statisticTimeInterval: number // 统计时间间隔单位ms默认值10000
fast: boolean // 是否启用快速模式(为了追求性能牺牲一部分特性),默认不启用
maxUploadSize: number // 最大上传文件大小multipart/form-data请求的总空间单位字节默认值104857600
cpu: number // CPU占用的核数默认为0即不做限制
memory: number // 内存单位M默认为0即不做限制
cpuMonitor: boolean // 在日志中记录CPU使用情况默认不开启
memoryMonitor: boolean // 在日志中记录内存使用情况,默认不开启
cpuLimitValue: number // CPU超过最高占用值10-100超过次数将自动重启如果CpuMonitor开启的话默认100
memoryLimitValue: number // 内存超过最高占用值10-100超过次数将自动重启如果MemoryMonitor开启的话默认95
cpuLimitTimes: number // CPU超过最高占用值超过次数1-100将报警如果CpuMonitor开启的话默认6即30秒内连续6次
memoryLimitTimes: number // 内存超过最高占用值超过次数1-100将报警如果MemoryMonitor开启的话默认6即30秒内连续6次
cookieScope: string // 启用Session时Cookie的有效范围host|domain|topDomain默认值为host
idServer: string // 用s.UniqueId、s.Id来生成唯一ID雪花算法时所需的redis服务器连接如果不指定将不能实现跨服务的全局唯一
keepKeyCase: boolean // 是否保持Key的首字母大小写默认保持设置为false则自动将首字母转为小写
indexFiles: string[] // 访问静态文件时的索引文件,默认为 index.html
indexDir: boolean // 访问目录时显示文件列表
readTimeout: number // 读取请求的超时时间单位ms
readHeaderTimeout: number // 读取请求头的超时时间单位ms
writeTimeout: number // 响应写入的超时时间单位ms
idleTimeout: number // 连接空闲超时时间单位ms
maxHeaderBytes: number // 请求头的最大字节数
maxHandlers: number // 每个连接的最大处理程序数量
maxConcurrentStreams: number // 每个连接的最大并发流数量
maxDecoderHeaderTableSize: number // 解码器头表的最大大小
maxEncoderHeaderTableSize: number // 编码器头表的最大大小
maxReadFrameSize: number // 单个帧的最大读取大小
maxUploadBufferPerConnection: number // 每个连接的最大上传缓冲区大小
maxUploadBufferPerStream: number // 每个流的最大上传缓冲区大小
// 其他配置 // gateway 的配置参数
sessionKey: string // HTTP头和Cookie中SessionID的Key客户端没有传递时服务端自动生成Header的优先级高于Cookie默认为 Session, 设置为空时表示不使用 proxy: Map<string, string> // 代理配置key为[host][path]value为代理的目标应用或URL
deviceKey: string // 标识设备ID的Key客户端没有传递时服务端自动生成Header的优先级高于Cookie默认为 Device 设置为空时表示不使用 rewrite: Map<string, string> // 重写请求路径key为[host][path]value为重写后的路径
clientKey: string // 标识客户端的Key默认为 Client对应的Header头为 ClientName 和 ClientVersion static: Map<string, string> // 静态文件目录key为[host][path]value为目录路径
userIdKey: string // session中userID的Key用于在日志中记录userId信息默认为 userId
sessionProvider: string // 指定一个redis连接例如redis://:sskey加密的密码@127.0.0.1:6379/15默认使用内存存储
sessionTimeout: number // session过期时间单位 秒,默认为 3600秒
authFieldMessage: string | Object // 身份验证失败时的消息,默认为 auth failed可以设置对象来返回JSON可以使用模版 {{TARGET_AUTHLEVEL}}、{{USER_AUTHLEVEL}}
verifyFieldMessage: string | Object // 参数验证失败时的消息,默认为 verify failed可以设置对象来返回JSON可以使用模版 {{FAILED_FIELDS}}
limitedMessage: string | Object // 访问受限时的消息,默认为 too many requests可以设置对象来返回JSON可以使用模版 {{LIMITED_FROM}}、{{LIMITED_VALUE}}
limiterRedis: string // 限流器使用的Redis连接默认使用内存存储
limiters: Map<string, LimiterConfig> // 限流器配置from 为数据来源例如ip、user、device、header.User-Agent、in.phone 等in表示从请求参数中获取time为时间间隔单位mstimes为 时间间隔内允许访问的次数
// gateway 的配置参数 // github.com/ssgo/discover 的配置参数
proxy: Map<string, string> // 代理配置key为[host][path]value为代理的目标应用或URL registry: string // 服务注册中心请配置一个有效的RedisURL默认值 redis://:@127.0.0.1:6379/15
rewrite: Map<string, string> // 重写请求路径key为[host][path]value为重写后的路径 app: string // 设置该值将会自动将服务注册到 discover
static: Map<string, string> // 静态文件目录key为[host][path]value为目录路径 weight: number // 节点的权重,默认值 100
accessTokens: Map<string, number> // 请求接口时使用指定的Access-Token进行验证值为Token对应的authLevel允许访问大于等于指定对应authLevel的接口
// github.com/ssgo/discover 的配置参数 calls: Map<string, string> // 配置将会调用的服务,格式:[1|2]:[http|https|h2c]:[AccessToken]默认协议为h2c如使用h2c协议可只提供 AccessToken
registry: string // 服务注册中心请配置一个有效的RedisURL默认值 redis://:@127.0.0.1:6379/15 callRetryTimes: number // 调用其他服务时最大重试次数,默认值 10
app: string // 设置该值将会自动将服务注册到 discover ipPrefix: string // discover服务发现时指定使用的IP网段默认排除 172.17.Docker
weight: number // 节点的权重,默认值 100
accessTokens: Map<string, number> // 请求接口时使用指定的Access-Token进行验证值为Token对应的authLevel允许访问大于等于指定对应authLevel的接口
calls: Map<string, string> // 配置将会调用的服务,格式:[1|2]:[http|https|h2c]:[AccessToken]默认协议为h2c如使用h2c协议可只提供 AccessToken
callRetryTimes: number // 调用其他服务时最大重试次数,默认值 10
ipPrefix: string // discover服务发现时指定使用的IP网段默认排除 172.17.Docker
} }
interface CertSet { export interface CertSet {
certFile: string certFile: string
keyFile: string keyFile: string
} }
interface PoolConfig { export interface PoolConfig {
min: number min: number
max: string max: string
idle: number idle: number
} }
interface LimiterConfig { export interface LimiterConfig {
from: string from: string
time: number time: number
times: number times: number
} }
interface RegisterOption { export interface RegisterOption {
authLevel: number authLevel: number
host: string host: string
method: string method: string
path: string path: string
memo: string memo: string
noBody: boolean noBody: boolean
noLog200: boolean noLog200: boolean
limiters: string[] limiters: string[]
verifies: Object verifies: Object
requires: string[] requires: string[]
onMessage: (params: OnMessageParams) => void ext: Object
onClose: (params: RequestParams) => void onMessage: (params: OnMessageParams) => void
onClose: (params: RequestParams) => void
} }
interface RequestParams { export interface RequestParams {
args: Object args: Object
headers: Object headers: Object
request: Request request: Request
client: WSClient client: WSClient
caller: Caller caller: Caller
session: Session session: Session
response: Response response: Response
logger: Logger logger: Logger
} }
interface OnMessageParams { export interface OnMessageParams {
type: string type: string
data: string | Object data: string | Object
client: WSClient client: WSClient
session: Session session: Session
logger: Logger logger: Logger
} }
interface WSClient { export interface WSClient {
id: string id: string
read: () => WSMessage read: () => WSMessage
write: (data: any) => void write: (data: any) => void
writeMessage: (type: string, data: any) => void writeMessage: (type: string, data: any) => void
ping: () => void ping: () => void
close: () => void close: () => void
} }
interface WSMessage { export interface WSMessage {
type: string type: string
data: string | Object data: string | Object
} }
interface Session { export interface Session {
set: (key: string | Object, value?: any) => void set: (key: string | Object, value?: any) => void
get: (...keys: string[]) => any | Object get: (...keys: string[]) => any | Object
remove: (...keys: string[]) => void remove: (...keys: string[]) => void
setAuthLevel: (authLevel: number) => void setAuthLevel: (authLevel: number) => void
save: () => void save: () => void
} }
interface Caller { export interface Caller {
get(url: string, headers?: Object): Result get(url: string, headers?: Object): Result
head(url: string, headers?: Object): Result head(url: string, headers?: Object): Result
post(url: string, data: any, headers?: Object): Result post(url: string, data: any, headers?: Object): Result
put(url: string, data: any, headers?: Object): Result put(url: string, data: any, headers?: Object): Result
delete(url: string, data: any, headers?: Object): Result delete(url: string, data: any, headers?: Object): Result
do(method: string, url: string, data: any, callback?: (data: string) => void, headers?: Object): Result do(method: string, url: string, data: any, callback?: (data: string) => void, headers?: Object): Result
} }
interface Result { export interface Result {
status: string status: string
statusCode: number statusCode: number
headers: Object headers: Object
bytes(): Uint8Array bytes(): Uint8Array
string(): string string(): string
object(): Object object(): Object
} }
interface CookieOption { export interface CookieOption {
path: string path: string
domain: string domain: string
expires: any expires: any
maxAge: number maxAge: number
secure: boolean secure: boolean
httpOnly: boolean httpOnly: boolean
} }
interface Request { export interface Request {
id: string id: string
proto: string proto: string
scheme: string scheme: string
host: string host: string
method: string method: string
path: string path: string
remoteAddr: string remoteAddr: string
realIP: string realIP: string
referer: string referer: string
userAgent: string userAgent: string
url: string url: string
contentLength: number contentLength: number
cookies: Object cookies: Object
headers: Object headers: Object
args: Object args: Object
files: Map<string, UploadFile> files: Map<string, UploadFile>
multiFiles: Map<string, UploadFile[]> multiFiles: Map<string, UploadFile[]>
makeURL: (path: string) => string makeURL: (path: string) => string
readAll: () => any readAll: () => any
read: (size: number) => any read: (size: number) => any
close: () => void close: () => void
get: (key: string) => any get: (key: string) => any
set: (key: string, value: any) => void set: (key: string, value: any) => void
getHeader: (key: string) => string getHeader: (key: string) => string
setHeader: (key: string, value: string) => void setHeader: (key: string, value: string) => void
setUserID: (id: string) => void setUserID: (id: string) => void
} }
interface Response { export interface Response {
id: string id: string
setStatus: (code: number) => void setStatus: (code: number) => void
setCookie: (name: string, value: string, option?: CookieOption) => void setCookie: (name: string, value: string, option?: CookieOption) => void
setHeader: (name: string, value: string) => void setHeader: (name: string, value: string) => void
addHeader: (name: string, value: string) => void addHeader: (name: string, value: string) => void
getHeader: (name: string) => string getHeader: (name: string) => string
write: (data: any) => number write: (data: any) => number
flush: () => void flush: () => void
sendFile: (contentType: string, filename: string) => void sendFile: (contentType: string, filename: string) => void
downloadFile: (contentType: string, filename: string, data: any) => void downloadFile: (contentType: string, filename: string, data: any) => void
location: (url: string) => void location: (url: string) => void
end: (data: any) => void
} }
interface Logger { export interface Logger {
debug: (message: string, info?: Object) => void debug: (message: string, info?: Object) => void
info: (message: string, info?: Object) => void info: (message: string, info?: Object) => void
warn: (message: string, info?: Object) => void warn: (message: string, info?: Object) => void
error: (message: string, info?: Object) => void error: (message: string, info?: Object) => void
} }
interface UploadFile { export interface UploadFile {
name: string name: string
size: number size: number
data: any data: any
} }

View File

@ -2,6 +2,7 @@ package service
import ( import (
"reflect" "reflect"
"strings"
"sync" "sync"
"time" "time"
@ -9,12 +10,14 @@ import (
"apigo.cc/gojs/goja" "apigo.cc/gojs/goja"
"github.com/ssgo/log" "github.com/ssgo/log"
"github.com/ssgo/redis" "github.com/ssgo/redis"
"github.com/ssgo/u"
) )
type Session struct { type Session struct {
id string id string
conn *redis.Redis conn *redis.Redis
data map[string]any data map[string]any
funcAuthCache map[string]bool
} }
var sessionRedis *redis.Redis var sessionRedis *redis.Redis
@ -40,9 +43,10 @@ func NewSession(id string, logger *log.Logger) *Session {
} }
} }
return &Session{ return &Session{
id: id, id: id,
conn: conn, conn: conn,
data: data, data: data,
funcAuthCache: map[string]bool{},
} }
} }
@ -102,6 +106,73 @@ func (session *Session) SetAuthLevel(argsIn goja.FunctionCall, vm *goja.Runtime)
return nil return nil
} }
func (session *Session) authFuncs(needFuncs []string) bool {
cacheKey := strings.Join(needFuncs, "; ")
if cachedResult, ok := session.funcAuthCache[cacheKey]; ok {
return cachedResult
}
normalAuthOk := 0
requiredAuthTotal := 0
requiredAuthOk := 0
isOk := false
if userFuncs, ok := session.data["funcs"].([]string); ok && len(userFuncs) > 0 {
if u.StringIn(userFuncs, "system.superAdmin.") {
isOk = true
} else {
// 统计必须有几个权限
for _, needFunc := range needFuncs {
if needFunc[0] == '&' {
requiredAuthTotal++
}
}
// 检查是否有匹配的权限(左匹配)
for _, needFunc := range needFuncs {
isRequired := false
if needFunc[0] == '&' {
// 必须有此权限
isRequired = true
needFunc = needFunc[1:]
}
for _, userFunc := range userFuncs {
if strings.HasPrefix(userFunc, needFunc) {
// 匹配成功
if isRequired {
// 必须有此权限并且匹配成功
requiredAuthOk++
} else {
// 普通权限匹配成功
normalAuthOk++
}
break
}
}
if normalAuthOk > 0 && requiredAuthOk == requiredAuthTotal {
// 普通权限匹配成功,并且必须有权限也匹配成功,直接返回
isOk = true
break
}
}
}
}
session.funcAuthCache[cacheKey] = isOk
return isOk
}
func (session *Session) AuthFuncs(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
var needFuncs []string
if args.Arguments[0].ExportType().Kind() == reflect.Slice {
// 第一个参数是数组
needFuncs = args.Arr(0).StrArray(0)
} else {
// 平铺的参数
needFuncs = args.StrArray(0)
}
return vm.ToValue(session.authFuncs(needFuncs))
}
func (session *Session) Save(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { func (session *Session) Save(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
if session.conn == nil { if session.conn == nil {
now := time.Now().Unix() now := time.Now().Unix()

299
task.go
View File

@ -1,162 +1,165 @@
package service package service
import ( // type Task struct {
"container/list" // spec string
"sync" // file string
// vm *gojs.Runtime
// lock sync.RWMutex
// mtime time.Time
// // policy string
// }
// var tasks []Task
// var tasksLock = sync.RWMutex{}
"apigo.cc/gojs" // var taskData = map[string]map[string]any{}
"apigo.cc/gojs/goja" // var taskDataLock = sync.RWMutex{}
)
var taskData = map[string]map[string]any{} // var taskList = map[string]*list.List{}
var taskDataLock = sync.RWMutex{} // var taskListLock = sync.RWMutex{}
var taskList = map[string]*list.List{} // func DataSet(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
var taskListLock = sync.RWMutex{} // args := gojs.MakeArgs(&argsIn, vm).Check(3)
// scope := args.Str(0)
// key := args.Str(1)
// value := args.Any(2)
// taskDataLock.Lock()
// defer taskDataLock.Unlock()
// if taskData[scope] == nil {
// taskData[scope] = map[string]any{}
// }
// taskData[scope][key] = value
// return nil
// }
func DataSet(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { // func DataGet(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(3) // args := gojs.MakeArgs(&argsIn, vm).Check(2)
scope := args.Str(0) // scope := args.Str(0)
key := args.Str(1) // key := args.Str(1)
value := args.Any(2) // taskDataLock.RLock()
taskDataLock.Lock() // defer taskDataLock.RUnlock()
defer taskDataLock.Unlock() // if taskData[scope] != nil {
if taskData[scope] == nil { // return vm.ToValue(taskData[scope][key])
taskData[scope] = map[string]any{} // }
} // return nil
taskData[scope][key] = value // }
return nil
}
func DataGet(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { // func DataKeys(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(2) // args := gojs.MakeArgs(&argsIn, vm).Check(1)
scope := args.Str(0) // scope := args.Str(0)
key := args.Str(1) // taskDataLock.RLock()
taskDataLock.RLock() // defer taskDataLock.RUnlock()
defer taskDataLock.RUnlock() // if taskData[scope] != nil {
if taskData[scope] != nil { // keys := make([]string, len(taskData[scope]))
return vm.ToValue(taskData[scope][key]) // i := 0
} // for key := range taskData[scope] {
return nil // keys[i] = key
} // i++
// }
// return vm.ToValue(keys)
// }
// return nil
// }
func DataKeys(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { // func DataCount(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1) // args := gojs.MakeArgs(&argsIn, vm).Check(1)
scope := args.Str(0) // scope := args.Str(0)
taskDataLock.RLock() // taskDataLock.RLock()
defer taskDataLock.RUnlock() // defer taskDataLock.RUnlock()
if taskData[scope] != nil { // if taskData[scope] != nil {
keys := make([]string, len(taskData[scope])) // return vm.ToValue(len(taskData[scope]))
i := 0 // }
for key := range taskData[scope] { // return vm.ToValue(0)
keys[i] = key // }
i++
}
return vm.ToValue(keys)
}
return nil
}
func DataCount(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { // func DataFetch(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1) // args := gojs.MakeArgs(&argsIn, vm).Check(1)
scope := args.Str(0) // scope := args.Str(0)
taskDataLock.RLock() // taskDataLock.RLock()
defer taskDataLock.RUnlock() // defer taskDataLock.RUnlock()
if taskData[scope] != nil { // if taskData[scope] != nil {
return vm.ToValue(len(taskData[scope])) // all := make(map[string]any)
} // for k, v := range taskData[scope] {
return vm.ToValue(0) // all[k] = v
} // }
// return vm.ToValue(all)
// }
// return nil
// }
func DataFetch(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { // func DataRemove(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1) // args := gojs.MakeArgs(&argsIn, vm).Check(1)
scope := args.Str(0) // scope := args.Str(0)
taskDataLock.RLock() // key := args.Str(1)
defer taskDataLock.RUnlock() // taskDataLock.Lock()
if taskData[scope] != nil { // defer taskDataLock.Unlock()
all := make(map[string]any) // if taskData[scope] != nil {
for k, v := range taskData[scope] { // if key != "" {
all[k] = v // delete(taskData[scope], key)
} // } else {
return vm.ToValue(all) // delete(taskData, scope)
} // }
return nil // }
} // return nil
// }
func DataRemove(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { // func ListPush(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1) // args := gojs.MakeArgs(&argsIn, vm).Check(2)
scope := args.Str(0) // scope := args.Str(0)
key := args.Str(1) // value := args.Any(1)
taskDataLock.Lock() // fromHead := args.Bool(2)
defer taskDataLock.Unlock() // taskListLock.Lock()
if taskData[scope] != nil { // defer taskListLock.Unlock()
if key != "" { // list1 := taskList[scope]
delete(taskData[scope], key) // if list1 == nil {
} else { // list1 = list.New()
delete(taskData, scope) // taskList[scope] = list1
} // }
} // if fromHead {
return nil // list1.PushFront(value)
} // } else {
// list1.PushBack(value)
// }
// return nil
// }
func ListPush(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { // func ListPop(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(2) // args := gojs.MakeArgs(&argsIn, vm).Check(1)
scope := args.Str(0) // scope := args.Str(0)
value := args.Any(1) // fromEnd := args.Bool(1)
fromHead := args.Bool(2) // taskListLock.Lock()
taskListLock.Lock() // var item *list.Element
defer taskListLock.Unlock() // defer taskListLock.Unlock()
list1 := taskList[scope] // list1 := taskList[scope]
if list1 == nil { // if list1 != nil {
list1 = list.New() // if fromEnd {
taskList[scope] = list1 // item = list1.Front()
} // } else {
if fromHead { // item = list1.Back()
list1.PushFront(value) // }
} else { // if item != nil {
list1.PushBack(value) // list1.Remove(item)
} // return vm.ToValue(item.Value)
return nil // }
} // }
// return nil
// }
func ListPop(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { // func ListCount(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1) // args := gojs.MakeArgs(&argsIn, vm).Check(1)
scope := args.Str(0) // scope := args.Str(0)
fromEnd := args.Bool(1) // taskListLock.RLock()
taskListLock.Lock() // defer taskListLock.RUnlock()
var item *list.Element // if taskList[scope] != nil {
defer taskListLock.Unlock() // return vm.ToValue(taskList[scope].Len())
list1 := taskList[scope] // }
if list1 != nil { // return vm.ToValue(0)
if fromEnd { // }
item = list1.Front()
} else {
item = list1.Back()
}
if item != nil {
list1.Remove(item)
return vm.ToValue(item.Value)
}
}
return nil
}
func ListCount(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { // func ListRemove(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1) // args := gojs.MakeArgs(&argsIn, vm).Check(1)
scope := args.Str(0) // scope := args.Str(0)
taskListLock.RLock() // taskListLock.Lock()
defer taskListLock.RUnlock() // defer taskListLock.Unlock()
if taskList[scope] != nil { // delete(taskList, scope)
return vm.ToValue(taskList[scope].Len()) // return nil
} // }
return vm.ToValue(0)
}
func ListRemove(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
scope := args.Str(0)
taskListLock.Lock()
defer taskListLock.Unlock()
delete(taskList, scope)
return nil
}

View File

@ -1,13 +1,12 @@
import service from "apigo.cc/gojs/service" import service from "apigo.cc/gojs/service"
import co from "apigo.cc/gojs/console" import rt from "apigo.cc/gojs/runtime"
import u from "apigo.cc/gojs/util"
function main() { function main() {
service.register({ path: '/echo2', noLog200: true }, ({ args, response }) => { service.register({ path: '/echo2', noLog200: true }, ({ args, response }) => {
// setTimeout(() => { // setTimeout(() => {
// response.end(args.name) // response.end(args.name)
// }, 10) // }, 10)
u.sleep(10) rt.sleep(10)
return args.name return args.name
}) })
} }

View File

@ -1,5 +1,6 @@
import s from "apigo.cc/gojs/service" import s from "apigo.cc/gojs/service"
import co from "apigo.cc/gojs/console" import co from "apigo.cc/gojs/console"
import task from "apigo.cc/gojs/task"
function main() { function main() {
s.register({ s.register({
@ -9,11 +10,13 @@ function main() {
}, },
onClose: ({ client }) => { onClose: ({ client }) => {
co.info('ws closed', client.id) co.info('ws closed', client.id)
s.dataRemove('wsTest', client.id) // s.dataRemove('wsTest', client.id)
task.remove('wsTest_' + client.id)
} }
}, ({ client }) => { }, ({ client }) => {
co.info('ws connected', client.id) co.info('ws connected', client.id)
s.dataSet('wsTest', client.id, client) // s.dataSet('wsTest', client.id, client)
task.set('wsTest_' + client.id, client)
client.write('Hello, World!') client.write('Hello, World!')
}) })
} }

View File

@ -2,12 +2,14 @@ package service_test
import ( import (
"fmt" "fmt"
"os"
"sync" "sync"
"time" "time"
"apigo.cc/gojs" "apigo.cc/gojs"
_ "apigo.cc/gojs/console" _ "apigo.cc/gojs/console"
_ "apigo.cc/gojs/http" _ "apigo.cc/gojs/http"
_ "apigo.cc/gojs/runtime"
"apigo.cc/gojs/service" "apigo.cc/gojs/service"
_ "apigo.cc/gojs/service" _ "apigo.cc/gojs/service"
_ "apigo.cc/gojs/util" _ "apigo.cc/gojs/util"
@ -38,6 +40,7 @@ func TestStartByPool(t *testing.T) {
gojs.ExportForDev() gojs.ExportForDev()
rt2 = gojs.New() rt2 = gojs.New()
u.CopyFile("api/echo.js", "api/echo_tmp.js")
err := rt2.StartFromFile("start.js") err := rt2.StartFromFile("start.js")
if err != nil { if err != nil {
t.Fatal("start failed", err) t.Fatal("start failed", err)
@ -216,7 +219,7 @@ func TestStopByPool(t *testing.T) {
t.Fatal("stop failed", err) t.Fatal("stop failed", err)
} }
gojs.WaitAll() gojs.WaitAll()
os.Remove("api/echo_tmp.js")
runtime.GC() runtime.GC()
ms3 := runtime.MemStats{} ms3 := runtime.MemStats{}
runtime.ReadMemStats(&ms3) runtime.ReadMemStats(&ms3)

View File

@ -1,14 +1,20 @@
package service_test package service_test
import ( import (
"fmt"
"os"
"strings"
"testing" "testing"
"time" "time"
"apigo.cc/gojs" "apigo.cc/gojs"
_ "apigo.cc/gojs/console" _ "apigo.cc/gojs/console"
_ "apigo.cc/gojs/file"
_ "apigo.cc/gojs/http" _ "apigo.cc/gojs/http"
_ "apigo.cc/gojs/runtime"
_ "apigo.cc/gojs/service" _ "apigo.cc/gojs/service"
_ "apigo.cc/gojs/util" _ "apigo.cc/gojs/util"
"github.com/ssgo/httpclient"
"github.com/ssgo/u" "github.com/ssgo/u"
) )
@ -20,6 +26,7 @@ const runTimes = 100
func TestStart(t *testing.T) { func TestStart(t *testing.T) {
gojs.ExportForDev() gojs.ExportForDev()
rt = gojs.New() rt = gojs.New()
u.CopyFile("api/echo.js", "api/echo_tmp.js")
err := rt.StartFromFile("start.js") err := rt.StartFromFile("start.js")
if err != nil { if err != nil {
t.Fatal("start failed", err) t.Fatal("start failed", err)
@ -41,80 +48,106 @@ func TestStatic(t *testing.T) {
} }
} }
// func TestJsEcho(t *testing.T) { func TestJsEcho(t *testing.T) {
// for i := 0; i < runTimes; i++ { for i := 0; i < runTimes; i++ {
// name := u.UniqueId() name := u.UniqueId()
// r, err := rt.RunCode("test('" + name + "')") r, err := rt.RunCode("test('" + name + "')")
// if err != nil { if err != nil {
// t.Fatal("test js get failed, got error", err) t.Fatal("test js get failed, got error", err)
// } else if r != name { } else if r != name {
// t.Fatal("test js get failed, name not match", r, name) t.Fatal("test js get failed, name not match", r, name)
// } }
// } }
// } }
// func TestGoEcho(t *testing.T) { func TestJsEcho2(t *testing.T) {
// hc := httpclient.GetClientH2C(0) for i := 0; i < runTimes; i++ {
// for i := 0; i < runTimes; i++ { name := u.UniqueId()
// name := u.UniqueId() r, err := rt.RunCode("test2('" + name + "')")
// r := hc.Get("http://" + addr + "/echo?name=" + name) if err != nil {
// if r.Error != nil { t.Fatal("test2 js get failed, got error", err)
// t.Fatal("test go get failed, got error", r.Error) } else if r != name {
// } else if r.String() != name { t.Fatal("test2 js get failed, name not match", r, name)
// t.Fatal("test go get failed, name not match", r, name) }
// } }
// } u.WriteFile("api/echo_tmp.js", strings.Replace(u.ReadFileN("api/echo.js"), "return args.name", "return args.name+'!!'", 1))
// } }
// func TestJsAsyncEcho(t *testing.T) { func TestGoEcho(t *testing.T) {
// ch := make(chan bool, runTimes) hc := httpclient.GetClientH2C(0)
// t1 := time.Now().UnixMilli() for i := 0; i < runTimes; i++ {
// for i := 0; i < runTimes; i++ { name := u.UniqueId()
// go func() { r := hc.Get("http://" + addr + "/echo?name=" + name)
// name := u.UniqueId() if r.Error != nil {
// r, err := rt.RunCode("test('" + name + "')") t.Fatal("test go get failed, got error", r.Error)
// ch <- true } else if r.String() != name {
// if err != nil { t.Fatal("test go get failed, name not match", r, name)
// t.Fatal("test js async get failed, got error", err) }
// } else if r != name { }
// t.Fatal("test js async get failed, name not match", r, name) }
// }
// }()
// }
// for i := 0; i < runTimes; i++ {
// <-ch
// }
// t2 := time.Now().UnixMilli() - t1
// fmt.Println(u.BGreen("js async test time:"), t2, "ms")
// }
// func TestGoAsyncEcho(t *testing.T) { func TestJsAsyncEcho(t *testing.T) {
// hc := httpclient.GetClientH2C(0) ch := make(chan bool, runTimes)
// ch := make(chan bool, runTimes*10) t1 := time.Now().UnixMilli()
// t1 := time.Now().UnixMilli() for i := 0; i < runTimes; i++ {
// lastName := "" go func() {
// lastResult := "" name := u.UniqueId()
// for i := 0; i < runTimes*10; i++ { r, err := rt.RunCode("test('" + name + "')")
// name := fmt.Sprint("N", i) ch <- true
// lastName = name if err != nil {
// go func() { t.Fatal("test js async get failed, got error", err)
// r := hc.Get("http://" + addr + "/echo?name=" + name) } else if r != name {
// lastResult = r.String() t.Fatal("test js async get failed, name not match", r, name)
// ch <- true }
// if r.Error != nil { }()
// t.Fatal("test go async get failed, got error", r.Error) }
// } else if r.String() != name { for i := 0; i < runTimes; i++ {
// t.Fatal("test go async get failed, name not match", r, name) <-ch
// } }
// }() t2 := time.Now().UnixMilli() - t1
// } fmt.Println(u.BGreen("js async test time:"), t2, "ms")
// for i := 0; i < runTimes*10; i++ { }
// <-ch
// } func TestGoAsyncEcho(t *testing.T) {
// t2 := time.Now().UnixMilli() - t1 hc := httpclient.GetClientH2C(0)
// fmt.Println(u.BGreen("go async test time:"), t2, "ms") ch := make(chan bool, runTimes*10)
// fmt.Println(u.BGreen("last name:"), lastName, lastResult) t1 := time.Now().UnixMilli()
// } lastName := ""
lastResult := ""
for i := 0; i < runTimes*10; i++ {
name := fmt.Sprint("N", i)
lastName = name
go func() {
r := hc.Get("http://" + addr + "/echo?name=" + name)
lastResult = r.String()
ch <- true
if r.Error != nil {
t.Fatal("test go async get failed, got error", r.Error)
} else if r.String() != name {
t.Fatal("test go async get failed, name not match", r, name)
}
}()
}
for i := 0; i < runTimes*10; i++ {
<-ch
}
t2 := time.Now().UnixMilli() - t1
fmt.Println(u.BGreen("go async test time:"), t2, "ms")
fmt.Println(u.BGreen("last name:"), lastName, lastResult)
}
func TestJsEcho2WithHotLoad(t *testing.T) {
time.Sleep(time.Second)
for i := 0; i < runTimes; i++ {
name := u.UniqueId()
r, err := rt.RunCode("test2('" + name + "')")
if err != nil {
t.Fatal("test2 js get failed, got error", err)
} else if r != name+"!!" {
t.Fatal("test2 js get failed, name not match", r, name)
}
}
}
func TestStop(t *testing.T) { func TestStop(t *testing.T) {
go func() { go func() {
@ -123,6 +156,7 @@ func TestStop(t *testing.T) {
}() }()
gojs.WaitAll() gojs.WaitAll()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
os.Remove("api/echo_tmp.js")
// if err != nil { // if err != nil {
// t.Fatal("stop failed", err) // t.Fatal("stop failed", err)
// } // }

View File

@ -6,7 +6,9 @@ import (
"apigo.cc/gojs" "apigo.cc/gojs"
_ "apigo.cc/gojs/console" _ "apigo.cc/gojs/console"
_ "apigo.cc/gojs/http" _ "apigo.cc/gojs/http"
_ "apigo.cc/gojs/runtime"
_ "apigo.cc/gojs/service" _ "apigo.cc/gojs/service"
_ "apigo.cc/gojs/task"
_ "apigo.cc/gojs/util" _ "apigo.cc/gojs/util"
"github.com/ssgo/u" "github.com/ssgo/u"
@ -43,7 +45,9 @@ func TestWS(t *testing.T) {
} }
func TestStopWSByPool(t *testing.T) { func TestStopWSByPool(t *testing.T) {
_, err := rt3.RunCode("s.stop()") fmt.Println(u.BGreen("stop ws"))
_, err := rt3.RunCode("s.stop();task.stop();")
fmt.Println(u.BGreen("stop ws ok"), err)
if err != nil { if err != nil {
t.Fatal("stop failed", err) t.Fatal("stop failed", err)
} }

View File

@ -1,7 +1,8 @@
import s from "apigo.cc/gojs/service" import s from "apigo.cc/gojs/service"
import http from "apigo.cc/gojs/http" import http from "apigo.cc/gojs/http"
import u from "apigo.cc/gojs/util" import rt from "apigo.cc/gojs/runtime"
import co from "apigo.cc/gojs/console" import co from "apigo.cc/gojs/console"
import file from "apigo.cc/gojs/file"
let h2c = http let h2c = http
let urlPrefix let urlPrefix
@ -33,16 +34,17 @@ function main() {
}, },
rewrite: { rewrite: {
'/echo2.js': '/echo.js' '/echo2.js': '/echo.js'
} },
hotLoad: 1,
}) })
s.register({ path: '/echo', noLog200: true }, ({ args, response }) => { s.register({ path: '/echo', noLog200: true }, ({ args, response }) => {
// setTimeout(() => { // setTimeout(() => {
// response.end(args.name) // response.end(args.name)
// }, 1) // }, 1)
u.sleep(1) rt.sleep(1)
return args.name return args.name
}) })
s.load('api/echo.js', { min: 20, max: 1000, idle: 100 }) s.load('api/echo_tmp.js', { min: 20, max: 1000, idle: 100 })
s.load('api/user.js') s.load('api/user.js')
let host = s.start() let host = s.start()
h2c = http.newH2C({ h2c = http.newH2C({
@ -89,7 +91,7 @@ function testUser() {
if (r.statusCode != 429) { if (r.statusCode != 429) {
return r return r
} }
u.sleep(100) rt.sleep(100)
// 测试限流器过期后允许的 1 次请求 // 测试限流器过期后允许的 1 次请求
r = h2c.get('/userInfo').object() r = h2c.get('/userInfo').object()

View File

@ -1,13 +1,16 @@
import s from "apigo.cc/gojs/service" import s from "apigo.cc/gojs/service"
import http from "apigo.cc/gojs/http" import http from "apigo.cc/gojs/http"
import u from "apigo.cc/gojs/util" import rt from "apigo.cc/gojs/runtime"
import co from "apigo.cc/gojs/console" import co from "apigo.cc/gojs/console"
import task from "apigo.cc/gojs/task"
let hc = http let hc = http
let urlPrefix let urlPrefix
function main() { function main() {
s.load('api/ws.js') s.load('api/ws.js')
s.task('task.js', 100) // s.task('task.js', 100)
task.addTask("@every 1s", 'task.js')
task.start()
let host = s.start() let host = s.start()
hc = http.new({ baseURL: 'http://' + host }) hc = http.new({ baseURL: 'http://' + host })
return host return host
@ -31,7 +34,7 @@ function testWS() {
co.info('test ws abc ok') co.info('test ws abc ok')
// ws.ping() // ws.ping()
u.sleep(10) rt.sleep(10)
let pc = ws.pingCount() let pc = ws.pingCount()
co.info('test ws ping ok', pc.pingTimes, pc.pongTimes) co.info('test ws ping ok', pc.pingTimes, pc.pongTimes)
@ -51,15 +54,13 @@ function testWS() {
return r return r
} }
co.info('test ws json ok') co.info('test ws json ok')
rt.sleep(2000)
u.sleep(1000)
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
let j = ws.read() let j = ws.read()
if (i !== j.data) { if (i !== j.data) {
return j return j
} }
} }
ws.close() ws.close()
return true return true
} }

View File

@ -1,30 +1,31 @@
import s from 'apigo.cc/gojs/service' import s from 'apigo.cc/gojs/service'
import co from 'apigo.cc/gojs/console' import co from 'apigo.cc/gojs/console'
import task from 'apigo.cc/gojs/task'
function onStart() { function onStart() {
co.info('task start') co.info('task start')
} }
let i = 0 let i = 0
function onRun() { function onRun() {
let connCount = s.dataCount('wsTest') let keys = task.keys('wsTest_')
if (connCount > 0) { // let connCount = s.dataCount('wsTest')
let conns = s.dataFetch('wsTest') if (keys.length > 0) {
for (let id in conns) { // let conns = s.dataFetch('wsTest')
let conn = conns[id] let conns = task.getAll('wsTest_')
try { for (let id in conns) {
conn.write(i++) let conn = conns[id]
} catch (e) { if (!conn.write(i++)) {
co.error(e) task.remove(id)
s.dataRemove('wsTest', id) }
} }
} }
} co.info('task run', keys.length)
co.info('task run', connCount)
} }
function onStop() { function onStop() {
s.dataRemove('wsTest') // s.dataRemove('wsTest')
co.info('task stop', s.dataCount('wsTest')) task.removeAll('wsTest_')
co.info('task stop')
} }

7
tests/tpl.js Normal file
View File

@ -0,0 +1,7 @@
import s from "apigo.cc/gojs/service"
function main(args) {
return s.tpl('tpl/page.html', { title: 'Abc' }, {
bb: text => { return '<b>' + text + '</b>' }
})
}

3
tests/tpl/header.html Normal file
View File

@ -0,0 +1,3 @@
<header>
<h1>Welcome to {{bb .title}}</h1>
</header>

18
tests/tpl/page.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{.title}}</title>
</head>
<body>
{{template "header.html" .}}
<pre>
hello world
</pre>
</body>
</html>

20
tests/tpl_test.go Normal file
View File

@ -0,0 +1,20 @@
package service_test
import (
"strings"
"testing"
"apigo.cc/gojs"
_ "apigo.cc/gojs/service"
"github.com/ssgo/u"
)
func TestTpl(t *testing.T) {
r, err := gojs.RunFile("tpl.js")
if err != nil {
t.Fatal("test static failed, got error", err)
}
if !strings.Contains(u.String(r), "<h1>Welcome to <b>Abc</b></h1>") {
t.Fatal("test tpl failed, name not match", r)
}
}

33
ws.go
View File

@ -18,7 +18,10 @@ func MakeWSClient(client *websocket.Conn, id string) gojs.Map {
"read": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { "read": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
typ, data, err := readWSMessage(client) typ, data, err := readWSMessage(client)
if err != nil { if err != nil {
panic(vm.NewGoError(err)) // panic(vm.NewGoError(err))
vm.SetData("_lastError", err)
gojs.GetLogger(vm).Error(err.Error())
return vm.ToValue(false)
} }
return vm.ToValue(gojs.Map{ return vm.ToValue(gojs.Map{
"type": typ, "type": typ,
@ -37,16 +40,22 @@ func MakeWSClient(client *websocket.Conn, id string) gojs.Map {
err = client.WriteMessage(websocket.TextMessage, u.JsonBytes(args.Any(0))) err = client.WriteMessage(websocket.TextMessage, u.JsonBytes(args.Any(0)))
} }
if err != nil { if err != nil {
panic(vm.NewGoError(err)) // panic(vm.NewGoError(err))
vm.SetData("_lastError", err)
gojs.GetLogger(vm).Error(err.Error())
return vm.ToValue(false)
} }
return nil return vm.ToValue(true)
}, },
"ping": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { "ping": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
err := client.WriteMessage(websocket.PingMessage, nil) err := client.WriteMessage(websocket.PingMessage, nil)
if err != nil { if err != nil {
panic(vm.NewGoError(err)) // panic(vm.NewGoError(err))
vm.SetData("_lastError", err)
gojs.GetLogger(vm).Error(err.Error())
return vm.ToValue(false)
} }
return nil return vm.ToValue(true)
}, },
"writeMessage": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { "writeMessage": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(2) args := gojs.MakeArgs(&argsIn, vm).Check(2)
@ -68,16 +77,22 @@ func MakeWSClient(client *websocket.Conn, id string) gojs.Map {
err = client.WriteMessage(websocket.TextMessage, args.Bytes(1)) err = client.WriteMessage(websocket.TextMessage, args.Bytes(1))
} }
if err != nil { if err != nil {
panic(vm.NewGoError(err)) // panic(vm.NewGoError(err))
vm.SetData("_lastError", err)
gojs.GetLogger(vm).Error(err.Error())
return vm.ToValue(false)
} }
return nil return vm.ToValue(true)
}, },
"close": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { "close": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
err := client.Close() err := client.Close()
if err != nil { if err != nil {
panic(vm.NewGoError(err)) // panic(vm.NewGoError(err))
vm.SetData("_lastError", err)
gojs.GetLogger(vm).Error(err.Error())
return vm.ToValue(false)
} }
return nil return vm.ToValue(true)
}, },
} }
} }