-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathyjs-server.js
More file actions
106 lines (87 loc) · 3.43 KB
/
yjs-server.js
File metadata and controls
106 lines (87 loc) · 3.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/usr/bin/env node
/**
* Hocuspocus WebSocket Server for collaborative editing
* Run this server to enable real-time collaboration with Yjs
*
* Usage: node yjs-server.js [port]
* Default port: 1234
*/
import { Server } from '@hocuspocus/server';
import * as Y from 'yjs';
const port = process.env.PORT || process.env.YJS_PORT || process.argv[2] || 1234;
// In-memory persistence. Hocuspocus unloads docs after a short debounce when
// no clients are connected — without an onLoadDocument that returns prior
// state, content is lost between the agent's write (writeToYjsRoom connects,
// transacts, disconnects) and the moment the user opens that file in their
// editor. We hold the encoded Y state per room so unloaded docs come back
// with their content. Process restart still wipes — swap for SQLite or
// Postgres extension when durability is needed.
const docStore = new Map();
const server = new Server({
port: Number(port),
// Logging
quiet: false,
// Increase max payload size to 100MB to handle large documents
maxPayload: 100 * 1024 * 1024,
async onLoadDocument({ documentName, document }) {
const stored = docStore.get(documentName);
if (stored) {
Y.applyUpdate(document, stored);
console.log(`[Hocuspocus] Restored ${stored.byteLength} bytes for ${documentName}`);
} else {
console.log(`[Hocuspocus] Document loaded (empty): ${documentName}`);
}
return document;
},
async onStoreDocument({ documentName, document }) {
docStore.set(documentName, Y.encodeStateAsUpdate(document));
console.log(`[Hocuspocus] Document stored: ${documentName} (${docStore.get(documentName).byteLength} bytes)`);
},
async onAuthenticate() {
return {
user: { id: 'anonymous', name: 'Anonymous User' },
};
},
// Plain HTTP /health endpoint for Render health checks + the homepage
// warmup ping. Hocuspocus' onRequest contract: throw a FALSY value to
// short-circuit the default "Welcome to Hocuspocus!" response. Throwing
// a truthy value (Error or non-empty string) rethrows and crashes the
// process under Node 20's strict unhandled-rejection mode.
// See: node_modules/@hocuspocus/server/src/Server.ts:118-135
async onRequest({ request, response }) {
if (request.url === '/health') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ ok: true, docs: docStore.size }));
throw '';
}
},
onConnect: (data) => {
console.log(`[Hocuspocus] ✅ Client connected to document: ${data.documentName}`);
},
onDisconnect: (data) => {
console.log(`[Hocuspocus] Client disconnected from: ${data.documentName}`);
},
onChange: (data) => {
console.log(`[Hocuspocus] Document changed: ${data.documentName}`);
},
onStateless: (data) => {
console.log(`[Hocuspocus] Received stateless message for: ${data.documentName}`);
},
});
await server.listen();
console.log(`🚀 Hocuspocus server running on ws://localhost:${port}`);
console.log(' Ready for collaborative editing!');
console.log(' Press Ctrl+C to stop');
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\n[Hocuspocus] Shutting down server...');
await server.destroy();
console.log('[Hocuspocus] Server closed');
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('\n[Hocuspocus] Shutting down server...');
await server.destroy();
console.log('[Hocuspocus] Server closed');
process.exit(0);
});