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

View File

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

View File

@@ -16,6 +16,7 @@ import (
"gopkg.d7z.net/gitea-pages/pkg/providers" "gopkg.d7z.net/gitea-pages/pkg/providers"
"gopkg.d7z.net/middleware/cache" "gopkg.d7z.net/middleware/cache"
"gopkg.d7z.net/middleware/kv" "gopkg.d7z.net/middleware/kv"
"gopkg.d7z.net/middleware/subscribe"
) )
var ( var (
@@ -64,15 +65,22 @@ func main() {
if !ok { if !ok {
log.Fatalln(errors.New("database not support cursor")) 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( pageServer, err := pkg.NewPageServer(
http.DefaultClient, http.DefaultClient,
backend, backend,
config.Domain, config.Domain,
config.Page.DefaultBranch, config.Page.DefaultBranch,
cdb, cdb,
event,
cacheMeta, cacheMeta,
config.Cache.MetaTTL, config.Cache.MetaTTL,
cacheBlob.Child("filter"), cacheBlob.Child("filter"),
config.Cache.BlobTTL,
config.ErrorHandler, config.ErrorHandler,
config.Filters, 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/go-task/slim-sprig/v3 v3.0.0
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/google/uuid v1.6.0 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/hashicorp/golang-lru/v2 v2.0.7
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0 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 gopkg.in/yaml.v3 v3.0.1
) )
@@ -35,7 +36,6 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // 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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/compress v1.18.1 // indirect
@@ -59,6 +59,6 @@ require (
golang.org/x/text v0.31.0 // indirect 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/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc 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 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.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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 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 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 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 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 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 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= 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 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= 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 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 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/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 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= 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 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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 h1:3JvqnWFLWzAoS57vLBT1LVePO3RqR32ijM3ZyjyoqyY=
gopkg.d7z.net/middleware v0.0.0-20251114145539-bb74bd940f32/go.mod h1:/1/EuissKhUbuhUe01rcWuwpA5mt7jASb4uKVNOLtR8= 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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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" "go.uber.org/zap"
"gopkg.d7z.net/middleware/kv" "gopkg.d7z.net/middleware/kv"
"gopkg.d7z.net/middleware/subscribe"
"gopkg.d7z.net/middleware/tools" "gopkg.d7z.net/middleware/tools"
) )
@@ -20,6 +21,9 @@ type FilterContext struct {
Cache *tools.TTLCache Cache *tools.TTLCache
OrgDB kv.CursorPagedKV OrgDB kv.CursorPagedKV
RepoDB kv.CursorPagedKV RepoDB kv.CursorPagedKV
Event subscribe.Subscriber
Kill func()
} }
type Params map[string]any type Params map[string]any

View File

@@ -58,8 +58,8 @@ func FilterInstGoJa(gl core.Params) (core.FilterInstance, error) {
stop := make(chan struct{}, 1) stop := make(chan struct{}, 1)
shutdown := make(chan struct{}, 1) shutdown := make(chan struct{}, 1)
defer close(shutdown) defer close(shutdown)
timeout, cancelFunc := context.WithTimeout(ctx, global.Timeout) timeout, timeoutCancelFunc := context.WithTimeout(ctx, global.Timeout)
defer cancelFunc() defer timeoutCancelFunc()
count := 0 count := 0
closers := NewClosers() closers := NewClosers()
defer closers.Close() defer closers.Close()
@@ -84,9 +84,12 @@ func FilterInstGoJa(gl core.Params) (core.FilterInstance, error) {
if err = KVInject(ctx, vm); err != nil { if err = KVInject(ctx, vm); err != nil {
panic(err) panic(err)
} }
if err = EventInject(ctx, vm); err != nil {
panic(err)
}
if global.EnableWebsocket { if global.EnableWebsocket {
var closer io.Closer var closer io.Closer
closer, err = WebsocketInject(vm, debug, request, cancelFunc) closer, err = WebsocketInject(ctx, vm, debug, request, timeoutCancelFunc)
if err != nil { if err != nil {
panic(err) 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 return nil
}, },
"getQuery": func(key string) string {
return req.URL.Query().Get(key)
},
"getHeader": func(name string) string { "getHeader": func(name string) string {
return req.Header.Get(name) return req.Header.Get(name)
}, },

View File

@@ -4,14 +4,16 @@ import (
"context" "context"
"io" "io"
"net/http" "net/http"
"time"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap" "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() closers := NewClosers()
return closers, jsCtx.GlobalObject().Set("websocket", func() (any, error) { return closers, jsCtx.GlobalObject().Set("websocket", func() (any, error) {
upgrader := websocket.Upgrader{} upgrader := websocket.Upgrader{}
@@ -20,9 +22,44 @@ func WebsocketInject(jsCtx *goja.Runtime, w http.ResponseWriter, request *http.R
return nil, err return nil, err
} }
cancelFunc() 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") zap.L().Debug("websocket upgrader created")
closers.AddCloser(conn.Close) closers.AddCloser(conn.Close)
return map[string]interface{}{ 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, "TypeTextMessage": websocket.TextMessage,
"TypeBinaryMessage": websocket.BinaryMessage, "TypeBinaryMessage": websocket.BinaryMessage,
"readText": func() (string, error) { "readText": func() (string, error) {
@@ -60,6 +97,9 @@ func WebsocketInject(jsCtx *goja.Runtime, w http.ResponseWriter, request *http.R
} }
return conn.WriteMessage(mType, dataRaw) return conn.WriteMessage(mType, dataRaw)
}, },
"ping": func() error {
return conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(1*time.Second))
},
}, nil }, nil
}) })
} }

View File

@@ -1,6 +1,7 @@
package pkg package pkg
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -18,6 +19,7 @@ import (
"gopkg.d7z.net/gitea-pages/pkg/utils" "gopkg.d7z.net/gitea-pages/pkg/utils"
"gopkg.d7z.net/middleware/cache" "gopkg.d7z.net/middleware/cache"
"gopkg.d7z.net/middleware/kv" "gopkg.d7z.net/middleware/kv"
"gopkg.d7z.net/middleware/subscribe"
"gopkg.d7z.net/middleware/tools" "gopkg.d7z.net/middleware/tools"
) )
@@ -28,9 +30,12 @@ type Server struct {
meta *core.PageDomain meta *core.PageDomain
db kv.CursorPagedKV db kv.CursorPagedKV
filterMgr map[string]core.FilterInstance 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) errorHandler func(w http.ResponseWriter, r *http.Request, err error)
} }
@@ -40,13 +45,15 @@ func NewPageServer(
domain string, domain string,
defaultBranch string, defaultBranch string,
db kv.CursorPagedKV, db kv.CursorPagedKV,
event subscribe.Subscriber,
cacheMeta kv.KV, cacheMeta kv.KV,
cacheTTL time.Duration, cacheMetaTTL time.Duration,
cacheBlob cache.Cache, cacheBlob cache.Cache,
cacheBlobTtl time.Duration,
errorHandler func(w http.ResponseWriter, r *http.Request, err error), errorHandler func(w http.ResponseWriter, r *http.Request, err error),
filterConfig map[string]map[string]any, filterConfig map[string]map[string]any,
) (*Server, error) { ) (*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) pageMeta := core.NewPageDomain(svcMeta, core.NewDomainAlias(db.Child("config").Child("alias")), domain, defaultBranch)
globCache, err := lru.New[string, glob.Glob](256) globCache, err := lru.New[string, glob.Glob](256)
if err != nil { if err != nil {
@@ -64,6 +71,8 @@ func NewPageServer(
filterMgr: defaultFilters, filterMgr: defaultFilters,
errorHandler: errorHandler, errorHandler: errorHandler,
cacheBlob: cacheBlob, cacheBlob: cacheBlob,
cacheBlobTtl: cacheBlobTtl,
event: event,
}, nil }, nil
} }
@@ -100,13 +109,17 @@ func (s *Server) Serve(writer *utils.WrittenResponseWriter, request *http.Reques
return err return err
} }
cancel, cancelFunc := context.WithCancel(request.Context())
filterCtx := core.FilterContext{ filterCtx := core.FilterContext{
PageContent: meta, PageContent: meta,
Context: request.Context(), Context: cancel,
PageVFS: core.NewPageVFS(s.backend, meta.Owner, meta.Repo, meta.CommitID), 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), 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), OrgDB: s.db.Child("org").Child(meta.Owner).(kv.CursorPagedKV),
RepoDB: s.db.Child("repo").Child(meta.Owner).Child(meta.Repo).(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)) 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/gitea-pages/pkg"
"gopkg.d7z.net/middleware/cache" "gopkg.d7z.net/middleware/cache"
"gopkg.d7z.net/middleware/kv" "gopkg.d7z.net/middleware/kv"
"gopkg.d7z.net/middleware/subscribe"
) )
type TestServer struct { type TestServer struct {
@@ -52,9 +53,11 @@ func NewTestServer(domain string) *TestServer {
domain, domain,
"gh-pages", "gh-pages",
memoryKV, memoryKV,
subscribe.NewMemorySubscriber(),
memoryKV.Child("cache"), memoryKV.Child("cache"),
0, 0,
memoryCache, memoryCache,
0,
func(w http.ResponseWriter, r *http.Request, err error) { func(w http.ResponseWriter, r *http.Request, err error) {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
http.Error(w, "page not found.", http.StatusNotFound) http.Error(w, "page not found.", http.StatusNotFound)