新增 event , 优化 websocket

This commit is contained in:
ExplodingDragon
2025-11-20 01:19:47 +08:00
parent 043b00bbb7
commit d6440ebb02
16 changed files with 646 additions and 13 deletions

View File

@@ -15,6 +15,7 @@ import (
"gopkg.d7z.net/gitea-pages/pkg/providers"
"gopkg.d7z.net/middleware/cache"
"gopkg.d7z.net/middleware/kv"
"gopkg.d7z.net/middleware/subscribe"
)
var (
@@ -56,8 +57,9 @@ func main() {
if err != nil {
zap.L().Fatal("failed to init memory provider", zap.Error(err))
}
subscriber := subscribe.NewMemorySubscriber()
server, err := pkg.NewPageServer(http.DefaultClient,
provider, domain, "gh-pages", memory, memory, 0, &nopCache{},
provider, domain, "gh-pages", memory, subscriber, memory, 0, &nopCache{}, 0,
func(w http.ResponseWriter, r *http.Request, err error) {
if errors.Is(err, os.ErrNotExist) {
http.Error(w, "page not found.", http.StatusNotFound)

View File

@@ -22,6 +22,7 @@ type Config struct {
Domain string `yaml:"domain"` // 基础域名
Database ConfigDatabase `yaml:"database"` // 配置
Event ConfigEvent `yaml:"event"` // 事件传递
Auth ConfigAuth `yaml:"auth"` // 后端认证配置
@@ -76,6 +77,9 @@ type ConfigPage struct {
type ConfigDatabase struct {
URL string `yaml:"url"`
}
type ConfigEvent struct {
URL string `yaml:"url"`
}
type ConfigProxy struct {
Enable bool `yaml:"enable"` // 是否允许反向代理
@@ -113,6 +117,9 @@ func LoadConfig(path string) (*Config, error) {
if c.Database.URL == "" {
return nil, errors.New("c is required")
}
if c.Event.URL == "" {
c.Event.URL = "memory://"
}
if c.StaticDir != "" {
stat, err := os.Stat(c.StaticDir)
if err != nil {

View File

@@ -16,6 +16,7 @@ import (
"gopkg.d7z.net/gitea-pages/pkg/providers"
"gopkg.d7z.net/middleware/cache"
"gopkg.d7z.net/middleware/kv"
"gopkg.d7z.net/middleware/subscribe"
)
var (
@@ -64,15 +65,22 @@ func main() {
if !ok {
log.Fatalln(errors.New("database not support cursor"))
}
event, err := subscribe.NewSubscriberFromURL(config.Event.URL)
if err != nil {
log.Fatalln(err)
}
defer event.Close()
pageServer, err := pkg.NewPageServer(
http.DefaultClient,
backend,
config.Domain,
config.Page.DefaultBranch,
cdb,
event,
cacheMeta,
config.Cache.MetaTTL,
cacheBlob.Child("filter"),
config.Cache.BlobTTL,
config.ErrorHandler,
config.Filters,
)

View File

@@ -0,0 +1,7 @@
routes:
- path: "sender"
js:
exec: "sender.js"
- path: "event"
js:
exec: "event.js"

View File

@@ -0,0 +1,23 @@
let name=request.getQuery("name")
if (name===""){
throw Error(`Missing name "${name}"`)
}
let ws = websocket();
event.subscribe("messages").on(function (msg){
ws.writeText(msg)
})
let shouldExit = false;
while (!shouldExit) {
let data = ws.readText();
switch (data) {
case "exit":
shouldExit = true;
break;
default:
event.put("messages",JSON.stringify({
name:name,
data:data
}));
break;
}
}

View File

@@ -0,0 +1,459 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket聊天客户端</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.chat-container {
width: 100%;
max-width: 900px;
height: 85vh;
background-color: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
background: linear-gradient(to right, #4a00e0, #8e2de2);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.header-right {
display: flex;
align-items: center;
gap: 15px;
}
.status {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
}
.status-connecting {
background-color: #ffc107;
}
.status-connected {
background-color: #28a745;
}
.status-disconnected {
background-color: #dc3545;
}
.connection-btn {
padding: 8px 16px;
border: none;
border-radius: 20px;
cursor: pointer;
font-weight: 600;
background: rgba(255, 255, 255, 0.2);
color: white;
transition: all 0.3s;
}
.connection-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.name-input-container {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.2);
padding: 6px 12px;
border-radius: 20px;
}
.name-input-container label {
font-size: 14px;
}
.name-input-container input {
padding: 4px 8px;
border: none;
border-radius: 15px;
background: rgba(255, 255, 255, 0.9);
color: #333;
width: 80px;
outline: none;
font-size: 14px;
}
.chat-area {
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: #f8f9fa;
display: flex;
flex-direction: column;
gap: 15px;
}
.message {
max-width: 70%;
padding: 12px 18px;
border-radius: 18px;
position: relative;
line-height: 1.4;
word-wrap: break-word;
}
.left-message {
align-self: flex-start;
background-color: white;
color: #333;
border: 1px solid #e0e0e0;
border-bottom-left-radius: 5px;
}
.right-message {
align-self: flex-end;
background: linear-gradient(to right, #4a00e0, #8e2de2);
color: white;
border-bottom-right-radius: 5px;
}
.message-time {
font-size: 11px;
margin-top: 5px;
opacity: 0.7;
text-align: right;
}
.message-name {
font-size: 12px;
margin-bottom: 5px;
font-weight: bold;
}
.input-area {
padding: 20px;
background-color: white;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 10px;
}
.input-area input {
flex: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 25px;
outline: none;
font-size: 16px;
transition: all 0.3s;
}
.input-area input:focus {
border-color: #8e2de2;
box-shadow: 0 0 0 2px rgba(142, 45, 226, 0.2);
}
.send-btn {
padding: 12px 25px;
border: none;
border-radius: 25px;
cursor: pointer;
font-weight: 600;
background: linear-gradient(to right, #4a00e0, #8e2de2);
color: white;
transition: all 0.3s;
}
.send-btn:hover {
background: linear-gradient(to right, #3a00b0, #7a1dc2);
transform: translateY(-2px);
}
.send-btn:disabled {
background: #cccccc;
cursor: not-allowed;
transform: none;
}
.empty-state {
text-align: center;
color: #6c757d;
margin-top: 50px;
}
.empty-state i {
font-size: 48px;
margin-bottom: 15px;
opacity: 0.5;
}
@media (max-width: 768px) {
.chat-container {
height: 95vh;
}
.message {
max-width: 85%;
}
.header {
flex-direction: column;
gap: 10px;
text-align: center;
}
.header-left, .header-right {
width: 100%;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="chat-container">
<div class="header">
<div class="header-left">
<h1>WebSocket聊天客户端</h1>
<div class="status">
<div class="status-indicator status-disconnected"></div>
<span id="status-text">未连接</span>
</div>
</div>
<div class="header-right">
<div class="name-input-container">
<label for="nameInput">姓名:</label>
<input type="text" id="nameInput" placeholder="输入姓名">
</div>
<button class="connection-btn" id="connectionButton" onclick="toggleConnection()">连接</button>
</div>
</div>
<div class="chat-area" id="chatArea">
<div class="empty-state">
<i>💬</i>
<p>连接服务器开始聊天</p>
</div>
</div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="输入消息..." disabled>
<button class="send-btn" id="sendButton" onclick="sendMessage()" disabled>发送</button>
</div>
</div>
<script>
let ws = null;
let isConnected = false;
let currentUserName = "";
const statusText = document.getElementById('status-text');
const statusIndicator = document.querySelector('.status-indicator');
const chatArea = document.getElementById('chatArea');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
const connectionButton = document.getElementById('connectionButton');
const nameInput = document.getElementById('nameInput');
// 生成4位随机数字作为默认用户名
function generateRandomUsername() {
return Math.floor(1000 + Math.random() * 9000).toString();
}
// 初始化
function init() {
const randomUsername = generateRandomUsername();
nameInput.value = randomUsername;
currentUserName = randomUsername;
}
function toggleConnection() {
if (isConnected) {
disconnect();
} else {
connect();
}
}
function connect() {
// 获取用户名,如果没有输入则使用默认值
const username = nameInput.value.trim() || currentUserName;
currentUserName = username;
updateStatus('正在连接...', 'status-connecting');
connectionButton.textContent = '连接中...';
try {
// 使用用户名作为查询参数
ws = new WebSocket(`/event?name=${encodeURIComponent(username)}`);
ws.onopen = () => {
isConnected = true;
updateStatus('已连接', 'status-connected');
connectionButton.textContent = '断开连接';
enableControls(true);
addSystemMessage('连接成功,可以开始聊天了');
};
ws.onmessage = event => {
try {
// 解析JSON数据
const data = JSON.parse(event.data);
// 根据name字段判断消息显示位置
if (data.name === currentUserName) {
// 如果是当前用户,显示在右侧
addMessage(data.data, 'right', data.name);
} else {
// 如果是其他用户,显示在左侧
addMessage(data.data, 'left', data.name);
}
} catch (e) {
// 如果不是JSON按原方式处理
addMessage(event.data, 'left', '系统');
}
};
ws.onclose = () => {
isConnected = false;
updateStatus('未连接', 'status-disconnected');
connectionButton.textContent = '连接';
enableControls(false);
addSystemMessage('连接已断开');
};
ws.onerror = error => {
isConnected = false;
updateStatus('连接错误', 'status-disconnected');
connectionButton.textContent = '连接';
enableControls(false);
addSystemMessage('连接错误,请检查服务器');
};
} catch (error) {
isConnected = false;
updateStatus('连接失败', 'status-disconnected');
connectionButton.textContent = '连接';
enableControls(false);
}
}
function disconnect() {
if (ws) {
ws.close();
ws = null;
}
isConnected = false;
connectionButton.textContent = '连接';
enableControls(false);
addSystemMessage('已断开连接');
}
function sendMessage() {
const message = messageInput.value.trim();
if (message) {
// 发送消息到服务器
ws.send(message);
// 清空输入框
messageInput.value = '';
}
}
function addMessage(message, type, senderName) {
const emptyState = document.querySelector('.empty-state');
if (emptyState) {
emptyState.remove();
}
const messageElement = document.createElement('div');
const time = new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
if (type === 'right') {
messageElement.className = 'message right-message';
messageElement.innerHTML = `
${message}
<div class="message-time">${time}</div>
`;
} else if (type === 'left') {
messageElement.className = 'message left-message';
messageElement.innerHTML = `
<div class="message-name">${senderName}</div>
${message}
<div class="message-time">${time}</div>
`;
} else {
messageElement.className = 'message system-message';
messageElement.style.alignSelf = 'center';
messageElement.style.maxWidth = '100%';
messageElement.style.backgroundColor = 'rgba(0,0,0,0.05)';
messageElement.style.color = '#6c757d';
messageElement.style.fontSize = '14px';
messageElement.style.textAlign = 'center';
messageElement.style.borderRadius = '10px';
messageElement.textContent = message;
}
chatArea.appendChild(messageElement);
chatArea.scrollTop = chatArea.scrollHeight;
}
function addSystemMessage(message) {
addMessage(message, 'system');
}
function updateStatus(message, statusClass) {
statusText.textContent = message;
statusIndicator.className = 'status-indicator ' + statusClass;
}
function enableControls(connected) {
messageInput.disabled = !connected;
sendButton.disabled = !connected;
nameInput.disabled = connected; // 连接时禁用姓名修改
}
messageInput.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
sendMessage();
}
});
// 初始化生成随机用户名
init();
</script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
let name=request.getQuery("name")
let message=request.getQuery("data")
event.put("messages", JSON.stringify({
name:name,
data:message
}));
// response.write(event.subscribe("messages").get())

6
go.mod
View File

@@ -10,11 +10,12 @@ require (
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/gobwas/glob v0.2.3
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0
gopkg.d7z.net/middleware v0.0.0-20251114145539-bb74bd940f32
gopkg.d7z.net/middleware v0.0.0-20251119134829-0c55a98e6495
gopkg.in/yaml.v3 v3.0.1
)
@@ -35,7 +36,6 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
@@ -59,6 +59,6 @@ require (
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

10
go.sum
View File

@@ -120,16 +120,22 @@ go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec=
go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -183,10 +189,14 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.d7z.net/middleware v0.0.0-20251114145539-bb74bd940f32 h1:3JvqnWFLWzAoS57vLBT1LVePO3RqR32ijM3ZyjyoqyY=
gopkg.d7z.net/middleware v0.0.0-20251114145539-bb74bd940f32/go.mod h1:/1/EuissKhUbuhUe01rcWuwpA5mt7jASb4uKVNOLtR8=
gopkg.d7z.net/middleware v0.0.0-20251119134829-0c55a98e6495 h1:LvjpmL0nkZZtrUXCFZGyoh8O2X9l2B7ZXFldOzN8ShI=
gopkg.d7z.net/middleware v0.0.0-20251119134829-0c55a98e6495/go.mod h1:/1/EuissKhUbuhUe01rcWuwpA5mt7jASb4uKVNOLtR8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -10,6 +10,7 @@ import (
"go.uber.org/zap"
"gopkg.d7z.net/middleware/kv"
"gopkg.d7z.net/middleware/subscribe"
"gopkg.d7z.net/middleware/tools"
)
@@ -20,6 +21,9 @@ type FilterContext struct {
Cache *tools.TTLCache
OrgDB kv.CursorPagedKV
RepoDB kv.CursorPagedKV
Event subscribe.Subscriber
Kill func()
}
type Params map[string]any

View File

@@ -58,8 +58,8 @@ func FilterInstGoJa(gl core.Params) (core.FilterInstance, error) {
stop := make(chan struct{}, 1)
shutdown := make(chan struct{}, 1)
defer close(shutdown)
timeout, cancelFunc := context.WithTimeout(ctx, global.Timeout)
defer cancelFunc()
timeout, timeoutCancelFunc := context.WithTimeout(ctx, global.Timeout)
defer timeoutCancelFunc()
count := 0
closers := NewClosers()
defer closers.Close()
@@ -84,9 +84,12 @@ func FilterInstGoJa(gl core.Params) (core.FilterInstance, error) {
if err = KVInject(ctx, vm); err != nil {
panic(err)
}
if err = EventInject(ctx, vm); err != nil {
panic(err)
}
if global.EnableWebsocket {
var closer io.Closer
closer, err = WebsocketInject(vm, debug, request, cancelFunc)
closer, err = WebsocketInject(ctx, vm, debug, request, timeoutCancelFunc)
if err != nil {
panic(err)
}

View File

@@ -0,0 +1,43 @@
package goja
import (
"github.com/dop251/goja"
"gopkg.d7z.net/gitea-pages/pkg/core"
)
func EventInject(ctx core.FilterContext, jsCtx *goja.Runtime) error {
return jsCtx.GlobalObject().Set("event", map[string]interface{}{
"subscribe": func(key string) (map[string]any, error) {
subscribe, err := ctx.Event.Subscribe(ctx, key)
if err != nil {
return nil, err
}
return map[string]any{
"on": func(f func(string)) {
go func() {
z:
for {
select {
case <-ctx.Done():
break z
case data := <-subscribe:
f(data)
}
}
}()
},
"get": func() (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
case data := <-subscribe:
return data, nil
}
},
}, nil
},
"put": func(key, value string) error {
return ctx.Event.Publish(ctx, key, value)
},
})
}

View File

@@ -50,6 +50,9 @@ func RequestInject(ctx core.FilterContext, jsCtx *goja.Runtime, req *http.Reques
}
return nil
},
"getQuery": func(key string) string {
return req.URL.Query().Get(key)
},
"getHeader": func(name string) string {
return req.Header.Get(name)
},

View File

@@ -4,14 +4,16 @@ import (
"context"
"io"
"net/http"
"time"
"github.com/dop251/goja"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"go.uber.org/zap"
"gopkg.d7z.net/gitea-pages/pkg/core"
)
func WebsocketInject(jsCtx *goja.Runtime, w http.ResponseWriter, request *http.Request, cancelFunc context.CancelFunc) (io.Closer, error) {
func WebsocketInject(ctx core.FilterContext, jsCtx *goja.Runtime, w http.ResponseWriter, request *http.Request, cancelFunc context.CancelFunc) (io.Closer, error) {
closers := NewClosers()
return closers, jsCtx.GlobalObject().Set("websocket", func() (any, error) {
upgrader := websocket.Upgrader{}
@@ -20,9 +22,44 @@ func WebsocketInject(jsCtx *goja.Runtime, w http.ResponseWriter, request *http.R
return nil, err
}
cancelFunc()
go func() {
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
f:
for {
select {
case <-ctx.Done():
break f
case <-ticker.C:
}
if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(5*time.Second)); err != nil {
zap.L().Debug("websocket ping failed", zap.Error(err))
ctx.Kill()
}
}
}()
zap.L().Debug("websocket upgrader created")
closers.AddCloser(conn.Close)
return map[string]interface{}{
"on": func(f func(mType int, message string)) {
go func() {
z:
for {
select {
case <-ctx.Done():
break z
default:
messageType, p, err := conn.ReadMessage()
if err != nil {
break z
}
f(messageType, string(p))
}
}
}()
},
"TypeTextMessage": websocket.TextMessage,
"TypeBinaryMessage": websocket.BinaryMessage,
"readText": func() (string, error) {
@@ -60,6 +97,9 @@ func WebsocketInject(jsCtx *goja.Runtime, w http.ResponseWriter, request *http.R
}
return conn.WriteMessage(mType, dataRaw)
},
"ping": func() error {
return conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(1*time.Second))
},
}, nil
})
}

View File

@@ -1,6 +1,7 @@
package pkg
import (
"context"
"errors"
"fmt"
"net/http"
@@ -18,6 +19,7 @@ import (
"gopkg.d7z.net/gitea-pages/pkg/utils"
"gopkg.d7z.net/middleware/cache"
"gopkg.d7z.net/middleware/kv"
"gopkg.d7z.net/middleware/subscribe"
"gopkg.d7z.net/middleware/tools"
)
@@ -28,9 +30,12 @@ type Server struct {
meta *core.PageDomain
db kv.CursorPagedKV
filterMgr map[string]core.FilterInstance
globCache *lru.Cache[string, glob.Glob]
cacheBlob cache.Cache
globCache *lru.Cache[string, glob.Glob]
cacheBlob cache.Cache
cacheBlobTtl time.Duration
event subscribe.Subscriber
errorHandler func(w http.ResponseWriter, r *http.Request, err error)
}
@@ -40,13 +45,15 @@ func NewPageServer(
domain string,
defaultBranch string,
db kv.CursorPagedKV,
event subscribe.Subscriber,
cacheMeta kv.KV,
cacheTTL time.Duration,
cacheMetaTTL time.Duration,
cacheBlob cache.Cache,
cacheBlobTtl time.Duration,
errorHandler func(w http.ResponseWriter, r *http.Request, err error),
filterConfig map[string]map[string]any,
) (*Server, error) {
svcMeta := core.NewServerMeta(client, backend, domain, cacheMeta, cacheTTL)
svcMeta := core.NewServerMeta(client, backend, domain, cacheMeta, cacheMetaTTL)
pageMeta := core.NewPageDomain(svcMeta, core.NewDomainAlias(db.Child("config").Child("alias")), domain, defaultBranch)
globCache, err := lru.New[string, glob.Glob](256)
if err != nil {
@@ -64,6 +71,8 @@ func NewPageServer(
filterMgr: defaultFilters,
errorHandler: errorHandler,
cacheBlob: cacheBlob,
cacheBlobTtl: cacheBlobTtl,
event: event,
}, nil
}
@@ -100,13 +109,17 @@ func (s *Server) Serve(writer *utils.WrittenResponseWriter, request *http.Reques
return err
}
cancel, cancelFunc := context.WithCancel(request.Context())
filterCtx := core.FilterContext{
PageContent: meta,
Context: request.Context(),
Context: cancel,
PageVFS: core.NewPageVFS(s.backend, meta.Owner, meta.Repo, meta.CommitID),
Cache: tools.NewTTLCache(s.cacheBlob.Child("filter").Child(meta.Owner).Child(meta.Repo).Child(meta.CommitID), time.Minute),
OrgDB: s.db.Child("org").Child(meta.Owner).(kv.CursorPagedKV),
RepoDB: s.db.Child("repo").Child(meta.Owner).Child(meta.Repo).(kv.CursorPagedKV),
Event: s.event.Child("domain").Child(meta.Owner).Child(meta.Repo),
Kill: cancelFunc,
}
zap.L().Debug("new request", zap.Any("request path", meta.Path))

View File

@@ -14,6 +14,7 @@ import (
"gopkg.d7z.net/gitea-pages/pkg"
"gopkg.d7z.net/middleware/cache"
"gopkg.d7z.net/middleware/kv"
"gopkg.d7z.net/middleware/subscribe"
)
type TestServer struct {
@@ -52,9 +53,11 @@ func NewTestServer(domain string) *TestServer {
domain,
"gh-pages",
memoryKV,
subscribe.NewMemorySubscriber(),
memoryKV.Child("cache"),
0,
memoryCache,
0,
func(w http.ResponseWriter, r *http.Request, err error) {
if errors.Is(err, os.ErrNotExist) {
http.Error(w, "page not found.", http.StatusNotFound)