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