feat: FlowStack 发布系统首个可运行版本 (gin + React)
- Go gin 后端:流水线/节点/连线/运行/日志/GitOps/插件 REST API, SQLite(GORM) 自动建表 + seed,模拟执行引擎 + SSE 实时日志推送, 前端产物 go:embed 嵌入单二进制,一个端口同时服务 API 与页面 - React 19 前端:画布式流水线编辑器(React Flow + Tekton 任务库)、 GitOps 部署、批量发布、发布记录等 14 个模块页面, tRPC 全部替换为 REST(TanStack Query) - 首次启动生成随机密码管理员账号(日志 + data/initial-admin-password.txt) - run.sh 一键构建启动(默认端口 9527,支持 start/stop/restart/build/dev)
This commit is contained in:
commit
04d1342b27
115 files changed
+23268
No files matched your search
@@ -0,0 +1,56 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Hub 按 runId 分组的 SSE 订阅中心
|
||||
type Hub struct {
|
||||
mu sync.Mutex
|
||||
subs map[string]map[chan []byte]struct{}
|
||||
}
|
||||
|
||||
func NewHub() *Hub {
|
||||
return &Hub{subs: make(map[string]map[chan []byte]struct{})}
|
||||
}
|
||||
|
||||
func (h *Hub) Subscribe(runId string) chan []byte {
|
||||
ch := make(chan []byte, 64)
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.subs[runId] == nil {
|
||||
h.subs[runId] = make(map[chan []byte]struct{})
|
||||
}
|
||||
h.subs[runId][ch] = struct{}{}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (h *Hub) Unsubscribe(runId string, ch chan []byte) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if set := h.subs[runId]; set != nil {
|
||||
delete(set, ch)
|
||||
if len(set) == 0 {
|
||||
delete(h.subs, runId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publish 向 runId 的所有订阅者推送一条 SSE 帧(event + data)
|
||||
func (h *Hub) Publish(runId, event string, payload any) {
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
frame := []byte(fmt.Sprintf("event: %s\ndata: %s\n\n", event, data))
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
for ch := range h.subs[runId] {
|
||||
select {
|
||||
case ch <- frame:
|
||||
default: // 订阅者消费过慢则丢弃,避免阻塞模拟引擎
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user