import http from 'node:http';import{readFile}from'node:fs/promises';import{resolve,extname,sep}from'node:path';import{pathToFileURL}from'node:url';import{WebSocketServer,WebSocket}from'ws';import * as Y from 'yjs';
export function createWorkshopServer({port=Number(process.env.PORT||8312),host='127.0.0.1',root=resolve('public')}={}){
 const rooms=new Map();const server=http.createServer(async(req,res)=>{try{if(!['GET','HEAD'].includes(req.method)){res.writeHead(405,{Allow:'GET, HEAD'}).end();return;}const url=new URL(req.url,'http://localhost');if(url.pathname==='/health'){res.writeHead(200,{'Content-Type':'application/json'}).end(JSON.stringify({ok:true,rooms:rooms.size}));return;}const p=resolve(root,'.'+decodeURIComponent(url.pathname).replace(/\/$/,'/ws.html'));if(!p.startsWith(root+sep)){res.writeHead(403).end();return;}const data=await readFile(p);const types={'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.txt':'text/plain; charset=utf-8','.json':'application/json'};res.writeHead(200,{'Content-Type':types[extname(p)]||'application/octet-stream','X-Content-Type-Options':'nosniff','Cache-Control':'no-store'}).end(req.method==='HEAD'?undefined:data);}catch{res.writeHead(404).end('Not found');}});
 const wss=new WebSocketServer({noServer:true,maxPayload:65536,perMessageDeflate:false});
 function send(ws,data){if(ws.readyState!==WebSocket.OPEN)return;if(ws.bufferedAmount>262144){ws.close(1013,'Slow client');return;}ws.send(data,{binary:true});}
 server.on('upgrade',(req,socket,head)=>{let u;try{u=new URL(req.url,'http://localhost');const origin=req.headers.origin;if(u.pathname!='/sync'||!/^[a-z0-9-]{1,32}$/.test(u.searchParams.get('room')||'')||!origin||new URL(origin).host!==req.headers.host||!['http:','https:'].includes(new URL(origin).protocol))throw Error('Forbidden');}catch{socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');return;}
 const key=u.searchParams.get('room');const existing=rooms.get(key);if((!existing&&rooms.size>=8)||(existing&&existing.clients.size>=8)||wss.clients.size>=32){socket.end('HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n');return;}
 wss.handleUpgrade(req,socket,head,ws=>{let room=rooms.get(key);if(!room){const doc=new Y.Doc();room={doc,clients:new Set()};rooms.set(key,room);doc.on('update',(data,origin)=>{for(const c of room.clients)if(c!==origin)send(c,data);});}room.clients.add(ws);send(ws,Y.encodeStateAsUpdate(room.doc));let windowStart=Date.now(),count=0;
 ws.on('message',(data,isBinary)=>{if(Date.now()-windowStart>1000){windowStart=Date.now();count=0;}if(++count>30){ws.close(1008,'Rate limit');return;}if(!isBinary){ws.close(1003,'Binary updates only');return;}const trial=new Y.Doc();try{Y.applyUpdate(trial,Y.encodeStateAsUpdate(room.doc));Y.applyUpdate(trial,new Uint8Array(data));if([...trial.share.keys()].some(k=>k!=='note')||trial.getText('note').length>4000||Y.encodeStateAsUpdate(trial).length>131072)throw Error('Document limit');Y.applyUpdate(room.doc,new Uint8Array(data),ws);}catch{ws.close(1008,'Invalid update or document limit');}finally{trial.destroy();}});
 ws.on('error',()=>{});ws.on('close',()=>{room.clients.delete(ws);if(!room.clients.size){room.doc.destroy();rooms.delete(key);}});
 });});
 return {server,wss,rooms,listen:()=>new Promise((ok,no)=>{server.once('error',no);server.listen(port,host,()=>ok(server.address()));}),close:async()=>{for(const ws of wss.clients)ws.terminate();await new Promise(r=>wss.close(r));if(server.listening)await new Promise(r=>server.close(r));for(const room of rooms.values())room.doc.destroy();rooms.clear();}};
}
if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href){const app=createWorkshopServer();const address=await app.listen();console.log(`Local teaching WebSocket: http://127.0.0.1:${address.port}/ws.html`);let stopping=false;const stop=async()=>{if(stopping)return;stopping=true;await app.close();process.exit(0);};process.on('SIGINT',stop);process.on('SIGTERM',stop);}
