自部署多端同步:服务端、实时协作、口令登录与油画加载动画
- 新增 Node 服务端(Hono + Hocuspocus + node:sqlite),数据保存在服务器 - 基于 Yjs 的多端实时同步,本机 IndexedDB 离线缓存,撤销只针对本机操作 - 空间(标签)有独立地址,增删改通过接口同步到所有设备 - 口令登录(签名 Cookie 会话、登录限流、同源校验) - 图片上传到服务器,过大照片上传前自动压缩 - 历史版本(自动/手动保存、一键恢复)、跨空间搜索、在线设备 - 随机油画加载动画(Painting Loaders,本地收录 p5 / p5.brush) - 选中样式与拖拽性能优化:拖动结束后才同步位置 - Docker / docker-compose 部署,README 与 CLAUDE.md 更新
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { createServer } from 'node:http'
|
||||
import type { Duplex } from 'node:stream'
|
||||
|
||||
import { getRequestListener } from '@hono/node-server'
|
||||
import type { WebSocketLike } from '@hocuspocus/server'
|
||||
import crossws from 'crossws/adapters/node'
|
||||
|
||||
import { createApp } from './app.ts'
|
||||
import { SESSION_COOKIE, SessionManager, isSameOrigin, readCookie } from './auth.ts'
|
||||
import { Collab } from './collab.ts'
|
||||
import { loadConfig } from './config.ts'
|
||||
import { Storage } from './db.ts'
|
||||
import { ImageStore } from './images.ts'
|
||||
|
||||
type ClientConnection = ReturnType<Collab['hocuspocus']['handleConnection']>
|
||||
|
||||
const config = loadConfig()
|
||||
const storage = new Storage(config.databaseFile)
|
||||
const sessions = new SessionManager(config.secret, config.password, config.sessionDays)
|
||||
const collab = new Collab(storage, sessions)
|
||||
await collab.start()
|
||||
|
||||
const app = createApp({ config, sessions, collab, images: new ImageStore(config.imagesDir) })
|
||||
const server = createServer(getRequestListener(app.fetch))
|
||||
|
||||
const connections = new WeakMap<object, ClientConnection>()
|
||||
const ws = crossws({
|
||||
hooks: {
|
||||
open(peer) {
|
||||
const connection = collab.hocuspocus.handleConnection(
|
||||
peer.websocket as unknown as WebSocketLike,
|
||||
peer.request as Request,
|
||||
)
|
||||
connections.set(peer, connection)
|
||||
},
|
||||
message(peer, message) {
|
||||
connections.get(peer)?.handleMessage(message.uint8Array())
|
||||
},
|
||||
close(peer, event) {
|
||||
connections.get(peer)?.handleClose({ code: event.code ?? 1000, reason: event.reason ?? '' })
|
||||
connections.delete(peer)
|
||||
},
|
||||
error(peer, error) {
|
||||
console.error('[boundless] websocket error', error)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
server.on('upgrade', (request, socket: Duplex, head) => {
|
||||
const url = new URL(request.url ?? '/', 'http://localhost')
|
||||
const forwardedHost = request.headers['x-forwarded-host']
|
||||
const allowed =
|
||||
url.pathname === '/collab' &&
|
||||
isSameOrigin(request.headers.origin, [
|
||||
Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost,
|
||||
request.headers.host,
|
||||
]) &&
|
||||
sessions.verify(readCookie(request.headers.cookie, SESSION_COOKIE))
|
||||
|
||||
if (!allowed) {
|
||||
socket.end('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n')
|
||||
return
|
||||
}
|
||||
void ws.handleUpgrade(request, socket, head)
|
||||
})
|
||||
|
||||
server.listen(config.port, config.host, () => {
|
||||
console.log(`[boundless] 已启动 http://${config.host}:${config.port}`)
|
||||
console.log(`[boundless] 数据目录 ${config.dataDir}`)
|
||||
if (!config.staticDir) console.log('[boundless] 未托管前端页面(开发模式请访问 Vite 地址)')
|
||||
})
|
||||
|
||||
let shuttingDown = false
|
||||
async function shutdown() {
|
||||
if (shuttingDown) return
|
||||
shuttingDown = true
|
||||
server.close()
|
||||
// 先把还没写盘的修改落盘,再关闭数据库
|
||||
await collab.stop()
|
||||
await collab.hocuspocus.closeConnections()
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
storage.close()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => void shutdown())
|
||||
process.on('SIGTERM', () => void shutdown())
|
||||
Reference in New Issue
Block a user