新增 event , 优化 websocket
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
7
examples/js_ws_event/.pages.yaml
Normal file
7
examples/js_ws_event/.pages.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
routes:
|
||||
- path: "sender"
|
||||
js:
|
||||
exec: "sender.js"
|
||||
- path: "event"
|
||||
js:
|
||||
exec: "event.js"
|
||||
23
examples/js_ws_event/event.js
Normal file
23
examples/js_ws_event/event.js
Normal 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;
|
||||
}
|
||||
}
|
||||
459
examples/js_ws_event/index.html
Normal file
459
examples/js_ws_event/index.html
Normal 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>
|
||||
8
examples/js_ws_event/sender.js
Normal file
8
examples/js_ws_event/sender.js
Normal 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
6
go.mod
@@ -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
10
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
43
pkg/filters/goja/var_event.go
Normal file
43
pkg/filters/goja/var_event.go
Normal 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)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user