Files
boundless/server/index.ts
吴璨 fad9760265 自部署多端同步:服务端、实时协作、口令登录与油画加载动画
- 新增 Node 服务端(Hono + Hocuspocus + node:sqlite),数据保存在服务器
- 基于 Yjs 的多端实时同步,本机 IndexedDB 离线缓存,撤销只针对本机操作
- 空间(标签)有独立地址,增删改通过接口同步到所有设备
- 口令登录(签名 Cookie 会话、登录限流、同源校验)
- 图片上传到服务器,过大照片上传前自动压缩
- 历史版本(自动/手动保存、一键恢复)、跨空间搜索、在线设备
- 随机油画加载动画(Painting Loaders,本地收录 p5 / p5.brush)
- 选中样式与拖拽性能优化:拖动结束后才同步位置
- Docker / docker-compose 部署,README 与 CLAUDE.md 更新
2026-10-21 18:09:03 +08:00

88 lines
3.0 KiB
TypeScript

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())