@verdant-web/server 1.7.8 → 1.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/cjs/Baselines.d.ts +14 -14
  2. package/dist/cjs/Baselines.js +77 -77
  3. package/dist/cjs/ClientConnection.d.ts +18 -18
  4. package/dist/cjs/ClientConnection.js +105 -105
  5. package/dist/cjs/MessageSender.d.ts +5 -5
  6. package/dist/cjs/MessageSender.js +2 -2
  7. package/dist/cjs/OperationHistory.d.ts +34 -34
  8. package/dist/cjs/OperationHistory.js +108 -108
  9. package/dist/cjs/Presence.d.ts +15 -15
  10. package/dist/cjs/Presence.js +59 -59
  11. package/dist/cjs/Profiles.d.ts +10 -10
  12. package/dist/cjs/Profiles.js +19 -19
  13. package/dist/cjs/ReplicaKeepaliveTimers.d.ts +17 -17
  14. package/dist/cjs/ReplicaKeepaliveTimers.js +30 -30
  15. package/dist/cjs/Replicas.d.ts +23 -23
  16. package/dist/cjs/Replicas.js +132 -132
  17. package/dist/cjs/Server.d.ts +121 -121
  18. package/dist/cjs/Server.js +385 -377
  19. package/dist/cjs/Server.js.map +1 -1
  20. package/dist/cjs/ServerLibrary.d.ts +63 -63
  21. package/dist/cjs/ServerLibrary.js +467 -467
  22. package/dist/cjs/TokenProvider.d.ts +16 -16
  23. package/dist/cjs/TokenProvider.js +26 -26
  24. package/dist/cjs/TokenVerifier.d.ts +16 -16
  25. package/dist/cjs/TokenVerifier.js +28 -28
  26. package/dist/cjs/files/FileMetadata.d.ts +28 -28
  27. package/dist/cjs/files/FileMetadata.js +46 -46
  28. package/dist/cjs/files/FileStorage.d.ts +35 -35
  29. package/dist/cjs/files/FileStorage.js +71 -71
  30. package/dist/cjs/index.d.ts +9 -9
  31. package/dist/cjs/index.js +11 -11
  32. package/dist/cjs/migrations/v0.sql.d.ts +2 -2
  33. package/dist/cjs/migrations/v0.sql.js +3 -3
  34. package/dist/cjs/migrations/v1.sql.d.ts +2 -2
  35. package/dist/cjs/migrations/v1.sql.js +3 -3
  36. package/dist/cjs/migrations/v2.sql.d.ts +2 -2
  37. package/dist/cjs/migrations/v2.sql.js +4 -4
  38. package/dist/cjs/migrations/v3.sql.d.ts +2 -2
  39. package/dist/cjs/migrations/v3.sql.js +4 -4
  40. package/dist/cjs/migrations/v4.sql.d.ts +2 -2
  41. package/dist/cjs/migrations/v4.sql.js +4 -4
  42. package/dist/cjs/migrations.d.ts +2 -2
  43. package/dist/cjs/migrations.js +32 -32
  44. package/dist/cjs/types.d.ts +22 -22
  45. package/dist/cjs/types.js +2 -2
  46. package/dist/esm/Baselines.d.ts +14 -14
  47. package/dist/esm/Baselines.js +73 -73
  48. package/dist/esm/ClientConnection.d.ts +18 -18
  49. package/dist/esm/ClientConnection.js +101 -101
  50. package/dist/esm/MessageSender.d.ts +5 -5
  51. package/dist/esm/MessageSender.js +1 -1
  52. package/dist/esm/OperationHistory.d.ts +34 -34
  53. package/dist/esm/OperationHistory.js +104 -104
  54. package/dist/esm/Presence.d.ts +15 -15
  55. package/dist/esm/Presence.js +52 -52
  56. package/dist/esm/Profiles.d.ts +10 -10
  57. package/dist/esm/Profiles.js +15 -15
  58. package/dist/esm/ReplicaKeepaliveTimers.d.ts +17 -17
  59. package/dist/esm/ReplicaKeepaliveTimers.js +26 -26
  60. package/dist/esm/Replicas.d.ts +23 -23
  61. package/dist/esm/Replicas.js +128 -128
  62. package/dist/esm/Server.d.ts +121 -121
  63. package/dist/esm/Server.js +378 -370
  64. package/dist/esm/Server.js.map +1 -1
  65. package/dist/esm/ServerLibrary.d.ts +63 -63
  66. package/dist/esm/ServerLibrary.js +463 -463
  67. package/dist/esm/TokenProvider.d.ts +16 -16
  68. package/dist/esm/TokenProvider.js +19 -19
  69. package/dist/esm/TokenVerifier.d.ts +16 -16
  70. package/dist/esm/TokenVerifier.js +21 -21
  71. package/dist/esm/files/FileMetadata.d.ts +28 -28
  72. package/dist/esm/files/FileMetadata.js +42 -42
  73. package/dist/esm/files/FileStorage.d.ts +35 -35
  74. package/dist/esm/files/FileStorage.js +44 -44
  75. package/dist/esm/index.d.ts +9 -9
  76. package/dist/esm/index.js +4 -4
  77. package/dist/esm/migrations/v0.sql.d.ts +2 -2
  78. package/dist/esm/migrations/v0.sql.js +1 -1
  79. package/dist/esm/migrations/v1.sql.d.ts +2 -2
  80. package/dist/esm/migrations/v1.sql.js +1 -1
  81. package/dist/esm/migrations/v2.sql.d.ts +2 -2
  82. package/dist/esm/migrations/v2.sql.js +2 -2
  83. package/dist/esm/migrations/v3.sql.d.ts +2 -2
  84. package/dist/esm/migrations/v3.sql.js +2 -2
  85. package/dist/esm/migrations/v4.sql.d.ts +2 -2
  86. package/dist/esm/migrations/v4.sql.js +2 -2
  87. package/dist/esm/migrations.d.ts +2 -2
  88. package/dist/esm/migrations.js +25 -25
  89. package/dist/esm/types.d.ts +22 -22
  90. package/dist/esm/types.js +1 -1
  91. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  92. package/dist/tsconfig.tsbuildinfo +1 -1
  93. package/package.json +2 -2
  94. package/src/Server.ts +9 -2
  95. package/dist/cjs/ServerStorage.d.ts +0 -35
  96. package/dist/cjs/ServerStorage.js +0 -60
  97. package/dist/cjs/ServerStorage.js.map +0 -1
  98. package/dist/esm/ServerStorage.d.ts +0 -44
  99. package/dist/esm/ServerStorage.js +0 -58
  100. package/dist/esm/ServerStorage.js.map +0 -1
@@ -1,378 +1,386 @@
1
- "use strict";
2
- var __rest = (this && this.__rest) || function (s, e) {
3
- var t = {};
4
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
- t[p] = s[p];
6
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
- t[p[i]] = s[p[i]];
10
- }
11
- return t;
12
- };
13
- var __importDefault = (this && this.__importDefault) || function (mod) {
14
- return (mod && mod.__esModule) ? mod : { "default": mod };
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.Server = void 0;
18
- const common_1 = require("@verdant-web/common");
19
- const events_1 = __importDefault(require("events"));
20
- const http_1 = require("http");
21
- const ws_1 = require("ws");
22
- const Profiles_js_1 = require("./Profiles.js");
23
- const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
24
- const url_1 = require("url");
25
- const ClientConnection_js_1 = require("./ClientConnection.js");
26
- const ReplicaKeepaliveTimers_js_1 = require("./ReplicaKeepaliveTimers.js");
27
- const TokenVerifier_js_1 = require("./TokenVerifier.js");
28
- const busboy_1 = __importDefault(require("busboy"));
29
- const stream_1 = require("stream");
30
- const FileMetadata_js_1 = require("./files/FileMetadata.js");
31
- const ServerLibrary_js_1 = require("./ServerLibrary.js");
32
- const migrations_js_1 = require("./migrations.js");
33
- class DefaultProfiles {
34
- constructor() {
35
- this.get = async (userId) => {
36
- return { id: userId };
37
- };
38
- }
39
- }
40
- class Server extends events_1.default {
41
- constructor(options) {
42
- super();
43
- this.clientConnections = new ClientConnection_js_1.ClientConnectionManager();
44
- this.keepalives = new ReplicaKeepaliveTimers_js_1.ReplicaKeepaliveTimers();
45
- this.connectionToReplicaIdMap = new WeakMap();
46
- this.log = () => { };
47
- this.__testMode = false;
48
- this.authorizeRequest = (req) => {
49
- return this.tokenVerifier.verifyToken(this.getRequestToken(req));
50
- };
51
- this.getRequestToken = (req) => {
52
- if (req.headers.authorization) {
53
- const [type, token] = req.headers.authorization.split(' ');
54
- if (type === 'Bearer') {
55
- return token;
56
- }
57
- }
58
- if (req.headers['sec-websocket-protocol']) {
59
- // hack: read the token from the websocket protocol header
60
- const [type, token] = req.headers['sec-websocket-protocol'].split(',');
61
- if (type === 'Bearer') {
62
- return token.trim();
63
- }
64
- }
65
- (0, common_1.assert)(req.url, 'Request URL is required');
66
- const url = new url_1.URL(req.url, 'http://localhost');
67
- const token = url.searchParams.get('token');
68
- (0, common_1.assert)(token, 'Token is required');
69
- return token;
70
- };
71
- this.internalServerHandleRequest = (req, res) => {
72
- const url = new url_1.URL(req.url || '', 'http://localhost');
73
- if (url.pathname.startsWith('lofi')) {
74
- if (url.pathname === 'lofi') {
75
- return this.handleRequest(req, res);
76
- }
77
- else if (url.pathname.startsWith('/lofi/files/')) {
78
- return this.handleFileRequest(req, res);
79
- }
80
- }
81
- else {
82
- res.writeHead(404);
83
- res.end();
84
- }
85
- };
86
- this.handleRequest = async (req, res) => {
87
- var _a;
88
- try {
89
- if (req.method === 'POST') {
90
- const info = this.authorizeRequest(req);
91
- const key = (0, common_1.generateId)();
92
- const finish = this.clientConnections.addRequest(info.libraryId, key, req, res, info);
93
- // FIXME: extremely naive compatibility with connect-like
94
- // servers...
95
- const body = (_a = req.body) !== null && _a !== void 0 ? _a : (await new Promise((resolve, reject) => {
96
- let body = '';
97
- req.on('data', (chunk) => {
98
- body += chunk;
99
- });
100
- req.on('end', () => {
101
- resolve(JSON.parse(body));
102
- });
103
- req.on('error', (e) => {
104
- reject(e);
105
- });
106
- }));
107
- for (const message of body.messages) {
108
- await this.handleMessage(key, info, message);
109
- }
110
- // update our keepalive timers for presence management
111
- const firstMessage = body.messages[0];
112
- if (firstMessage) {
113
- this.keepalives.refresh(info.libraryId, firstMessage.replicaId);
114
- }
115
- finish();
116
- this.emit('request', info);
117
- }
118
- }
119
- catch (e) {
120
- this.emit('error', e);
121
- res.writeHead(500);
122
- res.end();
123
- }
124
- };
125
- /**
126
- * Handles a multipart upload of a file from a verdant client. The upload
127
- * will include parameters for the file's ID, name, and type. The request
128
- * must be authenticated with a token to tie it to a library.
129
- */
130
- this.handleFileRequest = async (req, res) => {
131
- const fs = this.fileStorage;
132
- if (!fs) {
133
- this.emit('error', new Error('No file storage configured, but a client attempted to upload a file.'));
134
- res.writeHead(500);
135
- res.write('File storage is not configured');
136
- res.end();
137
- return;
138
- }
139
- // FIXME: rather than trying to support Express, I should
140
- // just expose regular methods you call from whatever HTTP handler...
141
- const url = new url_1.URL(req.originalUrl || req.baseUrl || req.url || '', 'http://localhost');
142
- const id = url.pathname.split('/').pop();
143
- if (!id || id === 'files') {
144
- res.writeHead(400);
145
- res.write('File ID is required to be in the URL path as the last parameter');
146
- res.end();
147
- return;
148
- }
149
- try {
150
- if (req.method === 'POST') {
151
- const info = this.authorizeRequest(req);
152
- await new Promise((resolve, reject) => {
153
- const bb = (0, busboy_1.default)({ headers: req.headers });
154
- bb.on('file', (fieldName, stream, fileInfo) => {
155
- // too many 'info's....
156
- const lofiFileInfo = {
157
- id,
158
- libraryId: info.libraryId,
159
- fileName: fileInfo.filename,
160
- type: fileInfo.mimeType,
161
- };
162
- // write metadata to storage
163
- try {
164
- this.fileMetadata.put(info.libraryId, lofiFileInfo);
165
- fs.put(stream, lofiFileInfo);
166
- }
167
- catch (e) {
168
- reject(e);
169
- }
170
- });
171
- bb.on('field', (fieldName, value) => {
172
- if (fieldName === 'file') {
173
- if (this.__testMode) {
174
- // this isn't right in the real world, but in testing it's
175
- // the only way we get file data.
176
- // we create a stream from the data and pass it as if it
177
- // were a file stream
178
- const stream = new stream_1.Readable();
179
- stream.push(value);
180
- stream.push(null);
181
- const fileInfo = {
182
- filename: 'test.txt',
183
- mimeType: 'text/plain',
184
- };
185
- bb.emit('file', fieldName, stream, fileInfo);
186
- }
187
- else {
188
- throw new Error('Invalid file upload');
189
- }
190
- }
191
- });
192
- req.pipe(bb);
193
- bb.on('finish', resolve);
194
- bb.on('error', reject);
195
- });
196
- this.log('File upload complete');
197
- res.writeHead(200);
198
- res.write(JSON.stringify({ success: true }));
199
- res.end();
200
- }
201
- else if (req.method === 'GET') {
202
- const info = this.authorizeRequest(req);
203
- const fileInfo = this.fileMetadata.get(info.libraryId, id);
204
- if (!fileInfo) {
205
- res.writeHead(404);
206
- res.end();
207
- return;
208
- }
209
- const url = await fs.getUrl({
210
- fileName: fileInfo.name,
211
- id: fileInfo.fileId,
212
- libraryId: info.libraryId,
213
- type: fileInfo.type,
214
- });
215
- res.writeHead(200, {
216
- 'Content-Type': 'application/json',
217
- });
218
- // we need to augment that data with the URL from the file backend.
219
- // and generally enforce the FileData interface here...
220
- const data = {
221
- id: fileInfo.fileId,
222
- url,
223
- remote: true,
224
- name: fileInfo.name,
225
- type: fileInfo.type,
226
- };
227
- res.write(JSON.stringify(data));
228
- res.end();
229
- }
230
- }
231
- catch (e) {
232
- this.log('Error handling file request', e);
233
- this.emit('error', e);
234
- res.writeHead(500);
235
- res.end();
236
- }
237
- };
238
- this.broadcast = (libraryId, message, omitKeys = []) => {
239
- this.clientConnections.broadcast(libraryId, message, omitKeys);
240
- };
241
- this.send = (libraryId, key, message) => {
242
- this.clientConnections.respond(libraryId, key, message);
243
- };
244
- this.handleMessage = (clientKey, info, message) => {
245
- this.log('Got message from user', info.userId, ', library', info.libraryId, JSON.stringify(message));
246
- return this.library.receive(message, clientKey, info);
247
- };
248
- this.handleConnection = (ws, req, info) => {
249
- const key = (0, common_1.generateId)();
250
- this.clientConnections.addSocket(info.libraryId, key, ws, info);
251
- ws.on('message', async (message) => {
252
- const data = JSON.parse(message.toString());
253
- if (data.replicaId) {
254
- this.connectionToReplicaIdMap.set(ws, data.replicaId);
255
- this.keepalives.refresh(info.libraryId, data.replicaId);
256
- }
257
- await this.handleMessage(key, info, data);
258
- });
259
- ws.on('close', () => {
260
- const replicaId = this.connectionToReplicaIdMap.get(ws);
261
- if (!replicaId) {
262
- this.emit('warning', 'Unknown replica disconnected', ws);
263
- return;
264
- }
265
- this.library.remove(info.libraryId, replicaId);
266
- this.emit('socket-close', info);
267
- });
268
- this.emit('socket-connection', info);
269
- };
270
- this.listen = (...params) => {
271
- this.httpServer.listen(...params);
272
- };
273
- this.close = async () => {
274
- await Promise.all([
275
- new Promise((resolve) => {
276
- setTimeout(() => {
277
- console.warn('HTTP server close timed out');
278
- resolve();
279
- }, 5 * 1000);
280
- this.httpServer.close(() => {
281
- resolve();
282
- this.log('HTTP server closed');
283
- });
284
- }),
285
- new Promise((resolve) => {
286
- setTimeout(() => {
287
- console.warn('Socket server close timed out');
288
- resolve();
289
- }, 5 * 1000);
290
- this.wss.close(() => {
291
- resolve();
292
- this.log('Socket server closed');
293
- });
294
- }),
295
- ]);
296
- this.library.close();
297
- };
298
- /**
299
- * Completely remove a library from the server.
300
- * This will remove all data associated with the library.
301
- * Use it very carefully! Data will still be stored on user
302
- * devices, so this is not as scary as it sounds - the next time
303
- * any replica connects, it will repopulate the library. But
304
- * you should still be careful.
305
- */
306
- this.evictLibrary = (libraryId) => {
307
- // disconnect all clients
308
- this.clientConnections.disconnectAll(libraryId);
309
- this.library.destroy(libraryId);
310
- this.log('Evicted library', libraryId);
311
- };
312
- this.getLibraryPresence = (libraryId) => {
313
- return this.library.getPresence(libraryId);
314
- };
315
- /**
316
- * Removes all replicas associated with a User ID from
317
- * the server. Consistency algorithms will no longer wait on these
318
- * replicas before confirming and squashing changes.
319
- */
320
- this.evictUser = (libraryId, userId) => {
321
- this.library.evictUser(libraryId, userId);
322
- this.log('Evicted user', userId, 'from library', libraryId);
323
- };
324
- /**
325
- * Retrieves a snapshot of the full document contents at this time.
326
- * Useful for capturing the shape of data for static usage outside
327
- * of the live system.
328
- */
329
- this.getDocumentSnapshot = (libraryId, collection, documentId) => {
330
- return this.library.getDocumentSnapshot(libraryId, (0, common_1.createOid)(collection, documentId, []));
331
- };
332
- this.__testMode =
333
- process.env.NODE_ENV === 'test' && process.env.VITEST === 'true';
334
- this.log = options.log || this.log;
335
- this.tokenVerifier = new TokenVerifier_js_1.TokenVerifier({
336
- secret: options.tokenSecret,
337
- });
338
- this.fileStorage = options.fileStorage;
339
- const db = (0, better_sqlite3_1.default)(options.databaseFile);
340
- (0, migrations_js_1.migrations)(db);
341
- this.fileMetadata = new FileMetadata_js_1.FileMetadata(db, options.fileConfig);
342
- this.library = new ServerLibrary_js_1.ServerLibrary({
343
- db,
344
- sender: this,
345
- profiles: new Profiles_js_1.UserProfileLoader(options.profiles || new DefaultProfiles()),
346
- log: this.log,
347
- disableRebasing: options.disableRebasing,
348
- fileMetadata: this.fileMetadata,
349
- fileStorage: this.fileStorage,
350
- replicaTruancyMinutes: options.replicaTruancyMinutes || 60 * 24 * 14,
351
- });
352
- this.library.subscribe('changes', (_a, operations, baselines) => {
353
- var { token } = _a, info = __rest(_a, ["token"]);
354
- this.emit('changes', info, operations, baselines);
355
- });
356
- this.wss = new ws_1.WebSocketServer({
357
- noServer: true,
358
- });
359
- this.wss.on('connection', this.handleConnection);
360
- this.httpServer =
361
- options.httpServer || new http_1.Server(this.internalServerHandleRequest);
362
- this.httpServer.on('upgrade', async (req, socket, head) => {
363
- try {
364
- const info = this.authorizeRequest(req);
365
- this.wss.handleUpgrade(req, socket, head, (ws) => {
366
- this.wss.emit('connection', ws, req, info);
367
- });
368
- }
369
- catch (e) {
370
- this.emit('error', e);
371
- socket.destroy();
372
- }
373
- });
374
- this.keepalives.subscribe('lost', this.library.remove);
375
- }
376
- }
377
- exports.Server = Server;
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.Server = void 0;
18
+ const common_1 = require("@verdant-web/common");
19
+ const events_1 = __importDefault(require("events"));
20
+ const http_1 = require("http");
21
+ const ws_1 = require("ws");
22
+ const Profiles_js_1 = require("./Profiles.js");
23
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
24
+ const url_1 = require("url");
25
+ const ClientConnection_js_1 = require("./ClientConnection.js");
26
+ const ReplicaKeepaliveTimers_js_1 = require("./ReplicaKeepaliveTimers.js");
27
+ const TokenVerifier_js_1 = require("./TokenVerifier.js");
28
+ const busboy_1 = __importDefault(require("busboy"));
29
+ const stream_1 = require("stream");
30
+ const FileMetadata_js_1 = require("./files/FileMetadata.js");
31
+ const ServerLibrary_js_1 = require("./ServerLibrary.js");
32
+ const migrations_js_1 = require("./migrations.js");
33
+ class DefaultProfiles {
34
+ constructor() {
35
+ this.get = async (userId) => {
36
+ return { id: userId };
37
+ };
38
+ }
39
+ }
40
+ class Server extends events_1.default {
41
+ constructor(options) {
42
+ super();
43
+ this.clientConnections = new ClientConnection_js_1.ClientConnectionManager();
44
+ this.keepalives = new ReplicaKeepaliveTimers_js_1.ReplicaKeepaliveTimers();
45
+ this.connectionToReplicaIdMap = new WeakMap();
46
+ this.log = () => { };
47
+ this.__testMode = false;
48
+ this.authorizeRequest = (req) => {
49
+ return this.tokenVerifier.verifyToken(this.getRequestToken(req));
50
+ };
51
+ this.getRequestToken = (req) => {
52
+ if (req.headers.authorization) {
53
+ const [type, token] = req.headers.authorization.split(' ');
54
+ if (type === 'Bearer') {
55
+ return token;
56
+ }
57
+ }
58
+ if (req.headers['sec-websocket-protocol']) {
59
+ // hack: read the token from the websocket protocol header
60
+ const [type, token] = req.headers['sec-websocket-protocol'].split(',');
61
+ if (type === 'Bearer') {
62
+ return token.trim();
63
+ }
64
+ }
65
+ (0, common_1.assert)(req.url, 'Request URL is required');
66
+ const url = new url_1.URL(req.url, 'http://localhost');
67
+ const token = url.searchParams.get('token');
68
+ (0, common_1.assert)(token, 'Token is required');
69
+ return token;
70
+ };
71
+ this.internalServerHandleRequest = (req, res) => {
72
+ const url = new url_1.URL(req.url || '', 'http://localhost');
73
+ if (url.pathname.startsWith('lofi')) {
74
+ if (url.pathname === 'lofi') {
75
+ return this.handleRequest(req, res);
76
+ }
77
+ else if (url.pathname.startsWith('/lofi/files/')) {
78
+ return this.handleFileRequest(req, res);
79
+ }
80
+ }
81
+ else {
82
+ res.writeHead(404);
83
+ res.end();
84
+ }
85
+ };
86
+ this.handleRequest = async (req, res) => {
87
+ var _a;
88
+ try {
89
+ if (req.method === 'POST') {
90
+ const info = this.authorizeRequest(req);
91
+ const key = (0, common_1.generateId)();
92
+ const finish = this.clientConnections.addRequest(info.libraryId, key, req, res, info);
93
+ // FIXME: extremely naive compatibility with connect-like
94
+ // servers...
95
+ const body = (_a = req.body) !== null && _a !== void 0 ? _a : (await new Promise((resolve, reject) => {
96
+ let body = '';
97
+ req.on('data', (chunk) => {
98
+ body += chunk;
99
+ });
100
+ req.on('end', () => {
101
+ resolve(JSON.parse(body));
102
+ });
103
+ req.on('error', (e) => {
104
+ reject(e);
105
+ });
106
+ }));
107
+ try {
108
+ for (const message of body.messages) {
109
+ await this.handleMessage(key, info, message);
110
+ }
111
+ }
112
+ catch (e) {
113
+ this.emit('error', e);
114
+ res.writeHead(500);
115
+ res.end();
116
+ return;
117
+ }
118
+ // update our keepalive timers for presence management
119
+ const firstMessage = body.messages[0];
120
+ if (firstMessage) {
121
+ this.keepalives.refresh(info.libraryId, firstMessage.replicaId);
122
+ }
123
+ finish();
124
+ this.emit('request', info);
125
+ }
126
+ }
127
+ catch (e) {
128
+ this.emit('error', e);
129
+ res.writeHead(500);
130
+ res.end();
131
+ }
132
+ };
133
+ /**
134
+ * Handles a multipart upload of a file from a verdant client. The upload
135
+ * will include parameters for the file's ID, name, and type. The request
136
+ * must be authenticated with a token to tie it to a library.
137
+ */
138
+ this.handleFileRequest = async (req, res) => {
139
+ const fs = this.fileStorage;
140
+ if (!fs) {
141
+ this.emit('error', new Error('No file storage configured, but a client attempted to upload a file.'));
142
+ res.writeHead(500);
143
+ res.write('File storage is not configured');
144
+ res.end();
145
+ return;
146
+ }
147
+ // FIXME: rather than trying to support Express, I should
148
+ // just expose regular methods you call from whatever HTTP handler...
149
+ const url = new url_1.URL(req.originalUrl || req.baseUrl || req.url || '', 'http://localhost');
150
+ const id = url.pathname.split('/').pop();
151
+ if (!id || id === 'files') {
152
+ res.writeHead(400);
153
+ res.write('File ID is required to be in the URL path as the last parameter');
154
+ res.end();
155
+ return;
156
+ }
157
+ try {
158
+ if (req.method === 'POST') {
159
+ const info = this.authorizeRequest(req);
160
+ await new Promise((resolve, reject) => {
161
+ const bb = (0, busboy_1.default)({ headers: req.headers });
162
+ bb.on('file', (fieldName, stream, fileInfo) => {
163
+ // too many 'info's....
164
+ const lofiFileInfo = {
165
+ id,
166
+ libraryId: info.libraryId,
167
+ fileName: fileInfo.filename,
168
+ type: fileInfo.mimeType,
169
+ };
170
+ // write metadata to storage
171
+ try {
172
+ this.fileMetadata.put(info.libraryId, lofiFileInfo);
173
+ fs.put(stream, lofiFileInfo);
174
+ }
175
+ catch (e) {
176
+ reject(e);
177
+ }
178
+ });
179
+ bb.on('field', (fieldName, value) => {
180
+ if (fieldName === 'file') {
181
+ if (this.__testMode) {
182
+ // this isn't right in the real world, but in testing it's
183
+ // the only way we get file data.
184
+ // we create a stream from the data and pass it as if it
185
+ // were a file stream
186
+ const stream = new stream_1.Readable();
187
+ stream.push(value);
188
+ stream.push(null);
189
+ const fileInfo = {
190
+ filename: 'test.txt',
191
+ mimeType: 'text/plain',
192
+ };
193
+ bb.emit('file', fieldName, stream, fileInfo);
194
+ }
195
+ else {
196
+ throw new Error('Invalid file upload');
197
+ }
198
+ }
199
+ });
200
+ req.pipe(bb);
201
+ bb.on('finish', resolve);
202
+ bb.on('error', reject);
203
+ });
204
+ this.log('File upload complete');
205
+ res.writeHead(200);
206
+ res.write(JSON.stringify({ success: true }));
207
+ res.end();
208
+ }
209
+ else if (req.method === 'GET') {
210
+ const info = this.authorizeRequest(req);
211
+ const fileInfo = this.fileMetadata.get(info.libraryId, id);
212
+ if (!fileInfo) {
213
+ res.writeHead(404);
214
+ res.end();
215
+ return;
216
+ }
217
+ const url = await fs.getUrl({
218
+ fileName: fileInfo.name,
219
+ id: fileInfo.fileId,
220
+ libraryId: info.libraryId,
221
+ type: fileInfo.type,
222
+ });
223
+ res.writeHead(200, {
224
+ 'Content-Type': 'application/json',
225
+ });
226
+ // we need to augment that data with the URL from the file backend.
227
+ // and generally enforce the FileData interface here...
228
+ const data = {
229
+ id: fileInfo.fileId,
230
+ url,
231
+ remote: true,
232
+ name: fileInfo.name,
233
+ type: fileInfo.type,
234
+ };
235
+ res.write(JSON.stringify(data));
236
+ res.end();
237
+ }
238
+ }
239
+ catch (e) {
240
+ this.log('Error handling file request', e);
241
+ this.emit('error', e);
242
+ res.writeHead(500);
243
+ res.end();
244
+ }
245
+ };
246
+ this.broadcast = (libraryId, message, omitKeys = []) => {
247
+ this.clientConnections.broadcast(libraryId, message, omitKeys);
248
+ };
249
+ this.send = (libraryId, key, message) => {
250
+ this.clientConnections.respond(libraryId, key, message);
251
+ };
252
+ this.handleMessage = (clientKey, info, message) => {
253
+ this.log('Got message from user', info.userId, ', library', info.libraryId, JSON.stringify(message));
254
+ return this.library.receive(message, clientKey, info);
255
+ };
256
+ this.handleConnection = (ws, req, info) => {
257
+ const key = (0, common_1.generateId)();
258
+ this.clientConnections.addSocket(info.libraryId, key, ws, info);
259
+ ws.on('message', async (message) => {
260
+ const data = JSON.parse(message.toString());
261
+ if (data.replicaId) {
262
+ this.connectionToReplicaIdMap.set(ws, data.replicaId);
263
+ this.keepalives.refresh(info.libraryId, data.replicaId);
264
+ }
265
+ await this.handleMessage(key, info, data);
266
+ });
267
+ ws.on('close', () => {
268
+ const replicaId = this.connectionToReplicaIdMap.get(ws);
269
+ if (!replicaId) {
270
+ this.emit('warning', 'Unknown replica disconnected', ws);
271
+ return;
272
+ }
273
+ this.library.remove(info.libraryId, replicaId);
274
+ this.emit('socket-close', info);
275
+ });
276
+ this.emit('socket-connection', info);
277
+ };
278
+ this.listen = (...params) => {
279
+ this.httpServer.listen(...params);
280
+ };
281
+ this.close = async () => {
282
+ await Promise.all([
283
+ new Promise((resolve) => {
284
+ setTimeout(() => {
285
+ console.warn('HTTP server close timed out');
286
+ resolve();
287
+ }, 5 * 1000);
288
+ this.httpServer.close(() => {
289
+ resolve();
290
+ this.log('HTTP server closed');
291
+ });
292
+ }),
293
+ new Promise((resolve) => {
294
+ setTimeout(() => {
295
+ console.warn('Socket server close timed out');
296
+ resolve();
297
+ }, 5 * 1000);
298
+ this.wss.close(() => {
299
+ resolve();
300
+ this.log('Socket server closed');
301
+ });
302
+ }),
303
+ ]);
304
+ this.library.close();
305
+ };
306
+ /**
307
+ * Completely remove a library from the server.
308
+ * This will remove all data associated with the library.
309
+ * Use it very carefully! Data will still be stored on user
310
+ * devices, so this is not as scary as it sounds - the next time
311
+ * any replica connects, it will repopulate the library. But
312
+ * you should still be careful.
313
+ */
314
+ this.evictLibrary = (libraryId) => {
315
+ // disconnect all clients
316
+ this.clientConnections.disconnectAll(libraryId);
317
+ this.library.destroy(libraryId);
318
+ this.log('Evicted library', libraryId);
319
+ };
320
+ this.getLibraryPresence = (libraryId) => {
321
+ return this.library.getPresence(libraryId);
322
+ };
323
+ /**
324
+ * Removes all replicas associated with a User ID from
325
+ * the server. Consistency algorithms will no longer wait on these
326
+ * replicas before confirming and squashing changes.
327
+ */
328
+ this.evictUser = (libraryId, userId) => {
329
+ this.library.evictUser(libraryId, userId);
330
+ this.log('Evicted user', userId, 'from library', libraryId);
331
+ };
332
+ /**
333
+ * Retrieves a snapshot of the full document contents at this time.
334
+ * Useful for capturing the shape of data for static usage outside
335
+ * of the live system.
336
+ */
337
+ this.getDocumentSnapshot = (libraryId, collection, documentId) => {
338
+ return this.library.getDocumentSnapshot(libraryId, (0, common_1.createOid)(collection, documentId, []));
339
+ };
340
+ this.__testMode =
341
+ process.env.NODE_ENV === 'test' && process.env.VITEST === 'true';
342
+ this.log = options.log || this.log;
343
+ this.tokenVerifier = new TokenVerifier_js_1.TokenVerifier({
344
+ secret: options.tokenSecret,
345
+ });
346
+ this.fileStorage = options.fileStorage;
347
+ const db = (0, better_sqlite3_1.default)(options.databaseFile);
348
+ (0, migrations_js_1.migrations)(db);
349
+ this.fileMetadata = new FileMetadata_js_1.FileMetadata(db, options.fileConfig);
350
+ this.library = new ServerLibrary_js_1.ServerLibrary({
351
+ db,
352
+ sender: this,
353
+ profiles: new Profiles_js_1.UserProfileLoader(options.profiles || new DefaultProfiles()),
354
+ log: this.log,
355
+ disableRebasing: options.disableRebasing,
356
+ fileMetadata: this.fileMetadata,
357
+ fileStorage: this.fileStorage,
358
+ replicaTruancyMinutes: options.replicaTruancyMinutes || 60 * 24 * 14,
359
+ });
360
+ this.library.subscribe('changes', (_a, operations, baselines) => {
361
+ var { token } = _a, info = __rest(_a, ["token"]);
362
+ this.emit('changes', info, operations, baselines);
363
+ });
364
+ this.wss = new ws_1.WebSocketServer({
365
+ noServer: true,
366
+ });
367
+ this.wss.on('connection', this.handleConnection);
368
+ this.httpServer =
369
+ options.httpServer || new http_1.Server(this.internalServerHandleRequest);
370
+ this.httpServer.on('upgrade', async (req, socket, head) => {
371
+ try {
372
+ const info = this.authorizeRequest(req);
373
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
374
+ this.wss.emit('connection', ws, req, info);
375
+ });
376
+ }
377
+ catch (e) {
378
+ this.emit('error', e);
379
+ socket.destroy();
380
+ }
381
+ });
382
+ this.keepalives.subscribe('lost', this.library.remove);
383
+ }
384
+ }
385
+ exports.Server = Server;
378
386
  //# sourceMappingURL=Server.js.map