新增 event , 优化 websocket
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
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/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
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.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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
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)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user