@verdant-web/server 3.3.10 → 4.0.0
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.
- package/bin/server.mjs +110 -107
- package/dist/esm/TokenVerifier.d.ts +1 -0
- package/dist/esm/TokenVerifier.js +3 -0
- package/dist/esm/TokenVerifier.js.map +1 -1
- package/dist/esm/adapters/node/errorHandler.d.ts +2 -0
- package/dist/esm/adapters/node/errorHandler.js +12 -0
- package/dist/esm/adapters/node/errorHandler.js.map +1 -0
- package/dist/esm/adapters/node/getRequestToken.d.ts +1 -0
- package/dist/esm/adapters/node/getRequestToken.js +25 -0
- package/dist/esm/adapters/node/getRequestToken.js.map +1 -0
- package/dist/esm/adapters/node/httpRouter.d.ts +47 -0
- package/dist/esm/adapters/node/httpRouter.js +88 -0
- package/dist/esm/adapters/node/httpRouter.js.map +1 -0
- package/dist/esm/adapters/node/index.d.ts +5 -0
- package/dist/esm/adapters/node/index.js +6 -0
- package/dist/esm/adapters/node/index.js.map +1 -0
- package/dist/esm/adapters/node/tokenMiddleware.d.ts +7 -0
- package/dist/esm/adapters/node/tokenMiddleware.js +22 -0
- package/dist/esm/adapters/node/tokenMiddleware.js.map +1 -0
- package/dist/esm/adapters/node/websocketHandler.d.ts +4 -0
- package/dist/esm/adapters/node/websocketHandler.js +54 -0
- package/dist/esm/adapters/node/websocketHandler.js.map +1 -0
- package/dist/esm/connections/ClientConnection.d.ts +25 -0
- package/dist/esm/{ClientConnection.js → connections/ClientConnection.js} +47 -38
- package/dist/esm/connections/ClientConnection.js.map +1 -0
- package/dist/esm/connections/MessageSender.d.ts +5 -0
- package/dist/esm/connections/MessageSender.js.map +1 -0
- package/dist/esm/connections/Presence.d.ts +23 -0
- package/dist/esm/connections/Presence.js +65 -0
- package/dist/esm/connections/Presence.js.map +1 -0
- package/dist/esm/core.d.ts +15 -0
- package/dist/esm/core.js +15 -0
- package/dist/esm/core.js.map +1 -0
- package/dist/esm/defaults.d.ts +2 -0
- package/dist/esm/defaults.js +3 -0
- package/dist/esm/defaults.js.map +1 -0
- package/dist/esm/files/FileStorage.d.ts +12 -3
- package/dist/esm/files/FileStorage.js +20 -0
- package/dist/esm/files/FileStorage.js.map +1 -1
- package/dist/esm/index.d.ts +7 -7
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internals.d.ts +14 -0
- package/dist/esm/internals.js +12 -0
- package/dist/esm/internals.js.map +1 -0
- package/dist/esm/libraries/Library.d.ts +78 -0
- package/dist/esm/{ServerLibrary.js → libraries/Library.js} +153 -144
- package/dist/esm/libraries/Library.js.map +1 -0
- package/dist/esm/{ServerLibrary.test.js → libraries/Library.test.js} +42 -33
- package/dist/esm/libraries/Library.test.js.map +1 -0
- package/dist/esm/libraries/generic.d.ts +8 -0
- package/dist/esm/libraries/generic.js +2 -0
- package/dist/esm/libraries/generic.js.map +1 -0
- package/dist/esm/libraries/singleNode.d.ts +53 -0
- package/dist/esm/libraries/singleNode.js +88 -0
- package/dist/esm/libraries/singleNode.js.map +1 -0
- package/dist/esm/logger.d.ts +1 -0
- package/dist/esm/logger.js +2 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/storage/Storage.d.ts +39 -38
- package/dist/esm/storage/StorageManager.d.ts +11 -0
- package/dist/esm/storage/StorageManager.js +32 -0
- package/dist/esm/storage/StorageManager.js.map +1 -0
- package/dist/esm/storage/index.d.ts +1 -2
- package/dist/esm/storage/index.js +1 -2
- package/dist/esm/storage/index.js.map +1 -1
- package/dist/esm/storage/sqlShard/Databases.d.ts +5 -3
- package/dist/esm/storage/sqlShard/Databases.js +11 -7
- package/dist/esm/storage/sqlShard/Databases.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlBaselines.d.ts +16 -17
- package/dist/esm/storage/sqlShard/SqlBaselines.js +55 -59
- package/dist/esm/storage/sqlShard/SqlBaselines.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlFileMetadata.d.ts +13 -13
- package/dist/esm/storage/sqlShard/SqlFileMetadata.js +44 -57
- package/dist/esm/storage/sqlShard/SqlFileMetadata.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlOperations.d.ts +12 -12
- package/dist/esm/storage/sqlShard/SqlOperations.js +38 -73
- package/dist/esm/storage/sqlShard/SqlOperations.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlReplicas.d.ts +18 -17
- package/dist/esm/storage/sqlShard/SqlReplicas.js +63 -100
- package/dist/esm/storage/sqlShard/SqlReplicas.js.map +1 -1
- package/dist/esm/storage/sqlShard/database.d.ts +25 -5
- package/dist/esm/storage/sqlShard/database.js +47 -15
- package/dist/esm/storage/sqlShard/database.js.map +1 -1
- package/dist/esm/storage/sqlShard/migrations/v1.d.ts +2 -3
- package/dist/esm/storage/sqlShard/migrations/v1.js +42 -47
- package/dist/esm/storage/sqlShard/migrations/v1.js.map +1 -1
- package/dist/esm/storage/sqlShard/migrations/v2.d.ts +2 -3
- package/dist/esm/storage/sqlShard/migrations/v2.js +5 -15
- package/dist/esm/storage/sqlShard/migrations/v2.js.map +1 -1
- package/dist/esm/storage/sqlShard/migrations.d.ts +5 -7
- package/dist/esm/storage/sqlShard/migrations.js +48 -1
- package/dist/esm/storage/sqlShard/migrations.js.map +1 -1
- package/dist/esm/storage/sqlShard/sqlShardStorage.d.ts +25 -4
- package/dist/esm/storage/sqlShard/sqlShardStorage.js +43 -40
- package/dist/esm/storage/sqlShard/sqlShardStorage.js.map +1 -1
- package/dist/esm/storage/sqlShard/tables.d.ts +9 -6
- package/dist/esm/types.d.ts +1 -2
- package/package.json +24 -13
- package/src/TokenVerifier.ts +4 -0
- package/src/adapters/node/errorHandler.ts +18 -0
- package/src/adapters/node/getRequestToken.ts +34 -0
- package/src/adapters/node/httpRouter.ts +139 -0
- package/src/adapters/node/index.ts +5 -0
- package/src/adapters/node/tokenMiddleware.ts +32 -0
- package/src/adapters/node/websocketHandler.ts +79 -0
- package/src/{ClientConnection.ts → connections/ClientConnection.ts} +64 -57
- package/src/connections/MessageSender.ts +6 -0
- package/src/connections/Presence.ts +95 -0
- package/src/core.ts +36 -0
- package/src/defaults.ts +2 -0
- package/src/files/FileStorage.ts +36 -3
- package/src/index.ts +7 -7
- package/src/internals.ts +14 -0
- package/src/{ServerLibrary.test.ts → libraries/Library.test.ts} +51 -49
- package/src/{ServerLibrary.ts → libraries/Library.ts} +203 -206
- package/src/libraries/generic.ts +9 -0
- package/src/libraries/singleNode.ts +145 -0
- package/src/logger.ts +4 -0
- package/src/storage/Storage.ts +37 -66
- package/src/storage/StorageManager.ts +39 -0
- package/src/storage/index.ts +1 -2
- package/src/storage/sqlShard/Databases.ts +26 -13
- package/src/storage/sqlShard/SqlBaselines.ts +58 -67
- package/src/storage/sqlShard/SqlFileMetadata.ts +51 -72
- package/src/storage/sqlShard/SqlOperations.ts +57 -81
- package/src/storage/sqlShard/SqlReplicas.ts +80 -121
- package/src/storage/sqlShard/database.ts +82 -18
- package/src/storage/sqlShard/migrations/v1.ts +42 -52
- package/src/storage/sqlShard/migrations/v2.ts +5 -19
- package/src/storage/sqlShard/migrations.ts +66 -1
- package/src/storage/sqlShard/sqlShardStorage.ts +56 -55
- package/src/storage/sqlShard/tables.ts +6 -6
- package/src/types.ts +1 -2
- package/dist/esm/ClientConnection.d.ts +0 -19
- package/dist/esm/ClientConnection.js.map +0 -1
- package/dist/esm/MessageSender.d.ts +0 -5
- package/dist/esm/MessageSender.js.map +0 -1
- package/dist/esm/Presence.d.ts +0 -18
- package/dist/esm/Presence.js +0 -65
- package/dist/esm/Presence.js.map +0 -1
- package/dist/esm/ReplicaKeepaliveTimers.d.ts +0 -17
- package/dist/esm/ReplicaKeepaliveTimers.js +0 -27
- package/dist/esm/ReplicaKeepaliveTimers.js.map +0 -1
- package/dist/esm/Server.d.ts +0 -166
- package/dist/esm/Server.js +0 -572
- package/dist/esm/Server.js.map +0 -1
- package/dist/esm/ServerLibrary.d.ts +0 -64
- package/dist/esm/ServerLibrary.js.map +0 -1
- package/dist/esm/ServerLibrary.test.js.map +0 -1
- package/dist/esm/storage/sql/SqlBaselines.d.ts +0 -22
- package/dist/esm/storage/sql/SqlBaselines.js +0 -107
- package/dist/esm/storage/sql/SqlBaselines.js.map +0 -1
- package/dist/esm/storage/sql/SqlFileMetadata.d.ts +0 -18
- package/dist/esm/storage/sql/SqlFileMetadata.js +0 -69
- package/dist/esm/storage/sql/SqlFileMetadata.js.map +0 -1
- package/dist/esm/storage/sql/SqlOperations.d.ts +0 -19
- package/dist/esm/storage/sql/SqlOperations.js +0 -122
- package/dist/esm/storage/sql/SqlOperations.js.map +0 -1
- package/dist/esm/storage/sql/SqlReplicas.d.ts +0 -32
- package/dist/esm/storage/sql/SqlReplicas.js +0 -163
- package/dist/esm/storage/sql/SqlReplicas.js.map +0 -1
- package/dist/esm/storage/sql/database.d.ts +0 -9
- package/dist/esm/storage/sql/database.js +0 -24
- package/dist/esm/storage/sql/database.js.map +0 -1
- package/dist/esm/storage/sql/migrations/v1.d.ts +0 -3
- package/dist/esm/storage/sql/migrations/v1.js +0 -59
- package/dist/esm/storage/sql/migrations/v1.js.map +0 -1
- package/dist/esm/storage/sql/migrations/v2.d.ts +0 -3
- package/dist/esm/storage/sql/migrations/v2.js +0 -16
- package/dist/esm/storage/sql/migrations/v2.js.map +0 -1
- package/dist/esm/storage/sql/migrations.d.ts +0 -7
- package/dist/esm/storage/sql/migrations.js +0 -4
- package/dist/esm/storage/sql/migrations.js.map +0 -1
- package/dist/esm/storage/sql/sqlStorage.d.ts +0 -6
- package/dist/esm/storage/sql/sqlStorage.js +0 -35
- package/dist/esm/storage/sql/sqlStorage.js.map +0 -1
- package/dist/esm/storage/sql/tables.d.ts +0 -44
- package/dist/esm/storage/sql/tables.js +0 -2
- package/dist/esm/storage/sql/tables.js.map +0 -1
- package/dist/esm/storage/sqlShard/transfer.d.ts +0 -7
- package/dist/esm/storage/sqlShard/transfer.js +0 -90
- package/dist/esm/storage/sqlShard/transfer.js.map +0 -1
- package/dist/esm/storage/sqlShard/transfer.test.d.ts +0 -1
- package/dist/esm/storage/sqlShard/transfer.test.js +0 -109
- package/dist/esm/storage/sqlShard/transfer.test.js.map +0 -1
- package/src/MessageSender.ts +0 -10
- package/src/Presence.ts +0 -86
- package/src/ReplicaKeepaliveTimers.ts +0 -34
- package/src/Server.ts +0 -859
- package/src/storage/sql/SqlBaselines.ts +0 -147
- package/src/storage/sql/SqlFileMetadata.ts +0 -94
- package/src/storage/sql/SqlOperations.ts +0 -160
- package/src/storage/sql/SqlReplicas.ts +0 -234
- package/src/storage/sql/database.ts +0 -32
- package/src/storage/sql/migrations/v1.ts +0 -64
- package/src/storage/sql/migrations/v2.ts +0 -20
- package/src/storage/sql/migrations.ts +0 -4
- package/src/storage/sql/sqlStorage.ts +0 -57
- package/src/storage/sql/tables.ts +0 -51
- package/src/storage/sqlShard/transfer.test.ts +0 -149
- package/src/storage/sqlShard/transfer.ts +0 -123
- /package/dist/esm/{MessageSender.js → connections/MessageSender.js} +0 -0
- /package/dist/esm/{ServerLibrary.test.d.ts → libraries/Library.test.d.ts} +0 -0
package/src/Server.ts
DELETED
|
@@ -1,859 +0,0 @@
|
|
|
1
|
-
import { Busboy } from '@fastify/busboy';
|
|
2
|
-
import {
|
|
3
|
-
assert,
|
|
4
|
-
ClientMessage,
|
|
5
|
-
createOid,
|
|
6
|
-
DocumentBaseline,
|
|
7
|
-
FileData,
|
|
8
|
-
generateId,
|
|
9
|
-
Operation,
|
|
10
|
-
ServerMessage,
|
|
11
|
-
VerdantError,
|
|
12
|
-
} from '@verdant-web/common';
|
|
13
|
-
import EventEmitter from 'events';
|
|
14
|
-
import { Server as HttpServer, IncomingMessage, ServerResponse } from 'http';
|
|
15
|
-
import internal, { Readable } from 'stream';
|
|
16
|
-
import { URL } from 'url';
|
|
17
|
-
import { WebSocket, WebSocketServer } from 'ws';
|
|
18
|
-
import { ClientConnectionManager } from './ClientConnection.js';
|
|
19
|
-
import { FileInfo, FileStorage } from './files/FileStorage.js';
|
|
20
|
-
import { MessageSender } from './MessageSender.js';
|
|
21
|
-
import { UserProfileLoader, UserProfiles } from './Profiles.js';
|
|
22
|
-
import { ReplicaKeepaliveTimers } from './ReplicaKeepaliveTimers.js';
|
|
23
|
-
import { ServerLibrary } from './ServerLibrary.js';
|
|
24
|
-
import { Storage, StorageFactory } from './storage/Storage.js';
|
|
25
|
-
import { TokenInfo, TokenVerifier } from './TokenVerifier.js';
|
|
26
|
-
|
|
27
|
-
export interface ServerOptions {
|
|
28
|
-
/**
|
|
29
|
-
* Attach to a pre-existing HTTP server. Provide this if you have another server,
|
|
30
|
-
* like an Express server, that you want to add lofi to. If provided, you don't
|
|
31
|
-
* need to call .listen()
|
|
32
|
-
*/
|
|
33
|
-
httpServer?: HttpServer;
|
|
34
|
-
/**
|
|
35
|
-
* The secret value used to generate tokens from your authentication endpoint.
|
|
36
|
-
* Used to verify and decode user identity and library access on connection.
|
|
37
|
-
*/
|
|
38
|
-
tokenSecret: string;
|
|
39
|
-
/**
|
|
40
|
-
* Your storage implementation. See the sqlStorage export for a built-in
|
|
41
|
-
* option.
|
|
42
|
-
*/
|
|
43
|
-
storage: StorageFactory;
|
|
44
|
-
/**
|
|
45
|
-
* Optionally provide an implementation of the UserProfiles interface to look up
|
|
46
|
-
* static user profile data to use in persistence. This data is considered trusted
|
|
47
|
-
* and can be used to identify users with information like name or profile image.
|
|
48
|
-
*/
|
|
49
|
-
profiles?: UserProfiles<any>;
|
|
50
|
-
/**
|
|
51
|
-
* How many minutes of inactivity before a replica is considered truant and
|
|
52
|
-
* removed from the library. Defaults to 30 days.
|
|
53
|
-
*/
|
|
54
|
-
replicaTruancyMinutes?: number;
|
|
55
|
-
/**
|
|
56
|
-
* Supply a logging function to log debug messages.
|
|
57
|
-
*/
|
|
58
|
-
log?: (...args: any[]) => void;
|
|
59
|
-
/**
|
|
60
|
-
* Disable history compaction. Storage usage will grow indefinitely. Not recommended.
|
|
61
|
-
*/
|
|
62
|
-
disableRebasing?: boolean;
|
|
63
|
-
/**
|
|
64
|
-
* Provide a FileStorage backend to accept file uploads. Without a file backend,
|
|
65
|
-
* users will not be able to synchronize files, and all files added on peer replicas
|
|
66
|
-
* will be forever unavailable.
|
|
67
|
-
*
|
|
68
|
-
* A default, filesystem-backed FileStorage implementation is provided as an export
|
|
69
|
-
* of this library.
|
|
70
|
-
*/
|
|
71
|
-
fileStorage?: FileStorage;
|
|
72
|
-
fileConfig?: { deleteExpirationDays?: number };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
class DefaultProfiles implements UserProfiles<{ id: string }> {
|
|
76
|
-
get = async (userId: string) => {
|
|
77
|
-
return { id: userId };
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface ServerEvents {
|
|
82
|
-
error: (err: any) => void;
|
|
83
|
-
warning: (msg: string, ...args: any[]) => void;
|
|
84
|
-
changes: (
|
|
85
|
-
info: Omit<TokenInfo, 'token'>,
|
|
86
|
-
operations: Operation[],
|
|
87
|
-
baselines: DocumentBaseline[],
|
|
88
|
-
) => void;
|
|
89
|
-
request: (info: TokenInfo) => void;
|
|
90
|
-
'socket-close': (info: TokenInfo) => void;
|
|
91
|
-
'socket-connection': (info: TokenInfo) => void;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export declare interface Server {
|
|
95
|
-
on<Event extends keyof ServerEvents>(
|
|
96
|
-
ev: Event,
|
|
97
|
-
cb: ServerEvents[Event],
|
|
98
|
-
): this;
|
|
99
|
-
off<Event extends keyof ServerEvents>(
|
|
100
|
-
ev: Event,
|
|
101
|
-
cb: ServerEvents[Event],
|
|
102
|
-
): this;
|
|
103
|
-
emit<Event extends keyof ServerEvents>(
|
|
104
|
-
ev: Event,
|
|
105
|
-
...args: Parameters<ServerEvents[Event]>
|
|
106
|
-
): boolean;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export class Server extends EventEmitter implements MessageSender {
|
|
110
|
-
private httpServer!: HttpServer;
|
|
111
|
-
private wss: WebSocketServer;
|
|
112
|
-
private fileStorage?: FileStorage;
|
|
113
|
-
private storage: Storage;
|
|
114
|
-
private library;
|
|
115
|
-
|
|
116
|
-
private clientConnections = new ClientConnectionManager();
|
|
117
|
-
private keepalives = new ReplicaKeepaliveTimers();
|
|
118
|
-
|
|
119
|
-
private tokenVerifier;
|
|
120
|
-
|
|
121
|
-
private connectionToReplicaIdMap = new WeakMap<WebSocket, string>();
|
|
122
|
-
|
|
123
|
-
private log: (...args: any[]) => void = () => {};
|
|
124
|
-
|
|
125
|
-
private __testMode = false;
|
|
126
|
-
|
|
127
|
-
constructor(options: ServerOptions) {
|
|
128
|
-
super();
|
|
129
|
-
|
|
130
|
-
this.__testMode =
|
|
131
|
-
process.env.NODE_ENV === 'test' && process.env.VITEST === 'true';
|
|
132
|
-
|
|
133
|
-
this.log = options.log || this.log;
|
|
134
|
-
|
|
135
|
-
this.tokenVerifier = new TokenVerifier({
|
|
136
|
-
secret: options.tokenSecret,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
this.fileStorage = options.fileStorage;
|
|
140
|
-
this.storage = options.storage({
|
|
141
|
-
fileDeleteExpirationDays: options.fileConfig?.deleteExpirationDays ?? 14,
|
|
142
|
-
replicaTruancyMinutes: options.replicaTruancyMinutes || 60 * 24 * 14,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
this.library = new ServerLibrary({
|
|
146
|
-
storage: this.storage,
|
|
147
|
-
sender: this,
|
|
148
|
-
profiles: new UserProfileLoader(
|
|
149
|
-
options.profiles || new DefaultProfiles(),
|
|
150
|
-
),
|
|
151
|
-
log: this.log,
|
|
152
|
-
disableRebasing: options.disableRebasing,
|
|
153
|
-
fileStorage: this.fileStorage,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
this.library.subscribe(
|
|
157
|
-
'changes',
|
|
158
|
-
({ token, ...info }, operations, baselines) => {
|
|
159
|
-
this.emit('changes', info, operations, baselines);
|
|
160
|
-
},
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
this.wss = new WebSocketServer({
|
|
164
|
-
noServer: true,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
this.wss.on('connection', this.handleConnection);
|
|
168
|
-
|
|
169
|
-
if (options.httpServer) {
|
|
170
|
-
// backwards compat with old httpServer option - it didn't attach path handlers
|
|
171
|
-
this.attach(options.httpServer, { httpPath: false });
|
|
172
|
-
} else {
|
|
173
|
-
this.attach(new HttpServer(this.createInternalRequestHandler()));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
this.keepalives.subscribe('lost', this.library.remove);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Attaches the Verdant server to an HttpServer instance, which
|
|
181
|
-
* allows it to handle requests and websockets required for the
|
|
182
|
-
* Verdant protocol.
|
|
183
|
-
*
|
|
184
|
-
* You can pass httpPath = false if you want to handle HTTP requests
|
|
185
|
-
* yourself, and this will only attach the websocket handling.
|
|
186
|
-
*/
|
|
187
|
-
attach = (
|
|
188
|
-
server: HttpServer,
|
|
189
|
-
options: { httpPath?: string | false } = {},
|
|
190
|
-
) => {
|
|
191
|
-
if (this.httpServer) {
|
|
192
|
-
this.httpServer.off('upgrade', this.handleUpgrade);
|
|
193
|
-
}
|
|
194
|
-
this.httpServer = server;
|
|
195
|
-
this.httpServer.on('upgrade', this.handleUpgrade);
|
|
196
|
-
|
|
197
|
-
if (options.httpPath !== false) {
|
|
198
|
-
this.httpServer.on(
|
|
199
|
-
'request',
|
|
200
|
-
this.createInternalRequestHandler(options.httpPath),
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Handles an HTTP upgrade request to a websocket connection.
|
|
207
|
-
*/
|
|
208
|
-
handleUpgrade = async (
|
|
209
|
-
req: IncomingMessage,
|
|
210
|
-
socket: internal.Duplex,
|
|
211
|
-
head: Buffer,
|
|
212
|
-
) => {
|
|
213
|
-
try {
|
|
214
|
-
const info = this.authorizeRequest(req);
|
|
215
|
-
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
216
|
-
this.wss.emit('connection', ws, req, info);
|
|
217
|
-
});
|
|
218
|
-
} catch (e) {
|
|
219
|
-
this.emit('error', e);
|
|
220
|
-
if (e instanceof VerdantError && e.httpStatus === 401) {
|
|
221
|
-
socket.write(
|
|
222
|
-
'HTTP/1.1 401 Unauthorized\r\n' +
|
|
223
|
-
'Connection: close\r\n' +
|
|
224
|
-
'Content-Length: 0\r\n' +
|
|
225
|
-
'\r\n',
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
socket.destroy();
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
private authorizeRequest = (req: IncomingMessage | Request) => {
|
|
233
|
-
return this.tokenVerifier.verifyToken(this.getRequestToken(req));
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
private getRequestToken = (req: IncomingMessage | Request) => {
|
|
237
|
-
if (isFetch(req)) {
|
|
238
|
-
const authHeader = req.headers.get('Authorization');
|
|
239
|
-
if (!authHeader) {
|
|
240
|
-
throw new VerdantError(
|
|
241
|
-
VerdantError.Code.NoToken,
|
|
242
|
-
undefined,
|
|
243
|
-
'Authorization header is required',
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
const [type, token] = authHeader.split(' ');
|
|
247
|
-
if (type === 'Bearer') {
|
|
248
|
-
return token;
|
|
249
|
-
}
|
|
250
|
-
return token;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (req.headers.authorization) {
|
|
254
|
-
const [type, token] = req.headers.authorization.split(' ');
|
|
255
|
-
if (type === 'Bearer') {
|
|
256
|
-
return token;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (req.headers['sec-websocket-protocol']) {
|
|
261
|
-
// hack: read the token from the websocket protocol header
|
|
262
|
-
const [type, token] = req.headers['sec-websocket-protocol'].split(',');
|
|
263
|
-
if (type === 'Bearer') {
|
|
264
|
-
return token.trim();
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
assert(req.url, 'Request URL is required');
|
|
269
|
-
const url = new URL(req.url, 'http://localhost');
|
|
270
|
-
const token = url.searchParams.get('token');
|
|
271
|
-
if (!token) {
|
|
272
|
-
throw new VerdantError(
|
|
273
|
-
VerdantError.Code.NoToken,
|
|
274
|
-
undefined,
|
|
275
|
-
'Token query parameter is required',
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
return token;
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
private createInternalRequestHandler = (pathPrefix = '/sync') => {
|
|
282
|
-
const handleRequest = (req: IncomingMessage, res: ServerResponse) => {
|
|
283
|
-
const url = new URL(req.url || '', 'http://localhost');
|
|
284
|
-
if (url.pathname.startsWith('sync')) {
|
|
285
|
-
if (url.pathname === 'sync') {
|
|
286
|
-
return this.handleRequest(req, res);
|
|
287
|
-
} else if (url.pathname.startsWith('/sync/files/')) {
|
|
288
|
-
return this.handleFileRequest(req, res);
|
|
289
|
-
}
|
|
290
|
-
} else {
|
|
291
|
-
res.writeHead(404);
|
|
292
|
-
res.end();
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
return handleRequest;
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Handles an HTTP request from a verdant client.
|
|
300
|
-
*/
|
|
301
|
-
handleRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
302
|
-
try {
|
|
303
|
-
if (req.method === 'POST') {
|
|
304
|
-
const info = this.authorizeRequest(req);
|
|
305
|
-
|
|
306
|
-
const key = generateId();
|
|
307
|
-
|
|
308
|
-
const finish = this.clientConnections.addRequest(
|
|
309
|
-
info.libraryId,
|
|
310
|
-
key,
|
|
311
|
-
req,
|
|
312
|
-
res,
|
|
313
|
-
info,
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
// FIXME: extremely naive compatibility with connect-like
|
|
317
|
-
// servers...
|
|
318
|
-
const body: {
|
|
319
|
-
messages: ClientMessage[];
|
|
320
|
-
} =
|
|
321
|
-
(req as any).body ??
|
|
322
|
-
(await new Promise<string>((resolve, reject) => {
|
|
323
|
-
let body = '';
|
|
324
|
-
req.on('data', (chunk) => {
|
|
325
|
-
body += chunk;
|
|
326
|
-
});
|
|
327
|
-
req.on('end', () => {
|
|
328
|
-
resolve(JSON.parse(body));
|
|
329
|
-
});
|
|
330
|
-
req.on('error', (e) => {
|
|
331
|
-
reject(e);
|
|
332
|
-
});
|
|
333
|
-
}));
|
|
334
|
-
|
|
335
|
-
await this.handleRequestBody(key, info, body);
|
|
336
|
-
|
|
337
|
-
finish();
|
|
338
|
-
|
|
339
|
-
this.emit('request', info);
|
|
340
|
-
}
|
|
341
|
-
} catch (e) {
|
|
342
|
-
return this.writeErrorResponse(e, res);
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Handles a "fetch" style request. Complement to handleRequest, for servers
|
|
348
|
-
* that use Request/Response style handlers.
|
|
349
|
-
*/
|
|
350
|
-
handleFetch = async (req: Request): Promise<Response> => {
|
|
351
|
-
try {
|
|
352
|
-
const info = this.authorizeRequest(req);
|
|
353
|
-
const key = generateId();
|
|
354
|
-
|
|
355
|
-
const finish = this.clientConnections.addFetch(
|
|
356
|
-
info.libraryId,
|
|
357
|
-
key,
|
|
358
|
-
req,
|
|
359
|
-
info,
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
const body = (await req.json()) as
|
|
363
|
-
| { messages: ClientMessage[] }
|
|
364
|
-
| null
|
|
365
|
-
| undefined;
|
|
366
|
-
|
|
367
|
-
if (!body) {
|
|
368
|
-
throw new VerdantError(VerdantError.Code.BodyRequired);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
await this.handleRequestBody(key, info, body);
|
|
372
|
-
|
|
373
|
-
const res = finish();
|
|
374
|
-
|
|
375
|
-
this.emit('request', info);
|
|
376
|
-
|
|
377
|
-
return res;
|
|
378
|
-
} catch (e) {
|
|
379
|
-
return this.getErrorResponse(e);
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
private handleRequestBody = async (
|
|
384
|
-
key: string,
|
|
385
|
-
info: TokenInfo,
|
|
386
|
-
body: { messages: ClientMessage[] },
|
|
387
|
-
) => {
|
|
388
|
-
for (const message of body.messages) {
|
|
389
|
-
await this.handleMessage(key, info, message);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// update our keepalive timers for presence management
|
|
393
|
-
const firstMessage = body.messages[0];
|
|
394
|
-
if (firstMessage) {
|
|
395
|
-
this.keepalives.refresh(info.libraryId, firstMessage.replicaId);
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
private writeErrorResponse(e: unknown, res: ServerResponse) {
|
|
400
|
-
this.emit('error', e);
|
|
401
|
-
this.log('error', 'Error handling request', e);
|
|
402
|
-
|
|
403
|
-
if (e instanceof VerdantError) {
|
|
404
|
-
// write error data to response
|
|
405
|
-
res.writeHead(e.httpStatus);
|
|
406
|
-
res.write(JSON.stringify(e.toResponse()));
|
|
407
|
-
} else {
|
|
408
|
-
res.writeHead(500);
|
|
409
|
-
res.write(
|
|
410
|
-
JSON.stringify(
|
|
411
|
-
new VerdantError(VerdantError.Code.Unexpected).toResponse(),
|
|
412
|
-
),
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
res.end();
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// for fetch-style
|
|
419
|
-
private getErrorResponse(e: unknown) {
|
|
420
|
-
this.emit('error', e);
|
|
421
|
-
this.log('error', 'Error handling request', e);
|
|
422
|
-
|
|
423
|
-
if (e instanceof VerdantError) {
|
|
424
|
-
return new Response(JSON.stringify(e.toResponse()), {
|
|
425
|
-
status: e.httpStatus,
|
|
426
|
-
headers: {
|
|
427
|
-
'Content-Type': 'application/json',
|
|
428
|
-
},
|
|
429
|
-
});
|
|
430
|
-
} else {
|
|
431
|
-
return new Response(
|
|
432
|
-
JSON.stringify(
|
|
433
|
-
new VerdantError(VerdantError.Code.Unexpected).toResponse(),
|
|
434
|
-
),
|
|
435
|
-
{
|
|
436
|
-
status: 500,
|
|
437
|
-
headers: {
|
|
438
|
-
'Content-Type': 'application/json',
|
|
439
|
-
},
|
|
440
|
-
},
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Handles a multipart upload of a file from a verdant client. The upload
|
|
447
|
-
* will include parameters for the file's ID, name, and type. The request
|
|
448
|
-
* must be authenticated with a token to tie it to a library.
|
|
449
|
-
*/
|
|
450
|
-
handleFileRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
451
|
-
this.log('info', 'Handling file request', req.url, req.method);
|
|
452
|
-
try {
|
|
453
|
-
const info = this.authorizeRequest(req);
|
|
454
|
-
|
|
455
|
-
const url = new URL(
|
|
456
|
-
(req as any).originalUrl || (req as any).baseUrl || req.url || '',
|
|
457
|
-
'http://localhost',
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
const id = url.pathname.split('/').pop();
|
|
461
|
-
|
|
462
|
-
if (!id || id === 'files') {
|
|
463
|
-
throw new VerdantError(VerdantError.Code.NotFound);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (req.method === 'POST') {
|
|
467
|
-
await this.streamIncomingFile({
|
|
468
|
-
req,
|
|
469
|
-
info,
|
|
470
|
-
headers: req.headers,
|
|
471
|
-
id,
|
|
472
|
-
});
|
|
473
|
-
this.log('info', 'File upload complete', id);
|
|
474
|
-
res.writeHead(200, {
|
|
475
|
-
'Content-Type': 'application/json',
|
|
476
|
-
});
|
|
477
|
-
res.write(JSON.stringify({ success: true }));
|
|
478
|
-
res.end();
|
|
479
|
-
} else if (req.method === 'GET') {
|
|
480
|
-
const data = await this.getFileData(info.libraryId, id);
|
|
481
|
-
res.writeHead(200, {
|
|
482
|
-
'Content-Type': 'application/json',
|
|
483
|
-
});
|
|
484
|
-
res.write(JSON.stringify(data));
|
|
485
|
-
res.end();
|
|
486
|
-
}
|
|
487
|
-
} catch (e) {
|
|
488
|
-
return this.writeErrorResponse(e, res);
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Handles a "fetch" style file request. Complement to handleFileRequest,
|
|
494
|
-
* for servers that use Request/Response style handlers.
|
|
495
|
-
*/
|
|
496
|
-
handleFileFetch = async (req: Request): Promise<Response> => {
|
|
497
|
-
this.log('info', 'Handling file fetch', req.url, req.method);
|
|
498
|
-
try {
|
|
499
|
-
const info = this.authorizeRequest(req);
|
|
500
|
-
|
|
501
|
-
const url = new URL(req.url, 'http://localhost');
|
|
502
|
-
|
|
503
|
-
const id = url.pathname.split('/').pop();
|
|
504
|
-
|
|
505
|
-
if (!id || id === 'files') {
|
|
506
|
-
throw new VerdantError(VerdantError.Code.NotFound);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (req.method === 'POST') {
|
|
510
|
-
if (!req.body) {
|
|
511
|
-
throw new VerdantError(VerdantError.Code.InvalidRequest);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const headersAsRecord = Array.from(req.headers.entries()).reduce(
|
|
515
|
-
(acc, [key, value]) => {
|
|
516
|
-
acc[key] = value;
|
|
517
|
-
return acc;
|
|
518
|
-
},
|
|
519
|
-
{} as Record<string, string | string[] | undefined>,
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
await this.streamIncomingFile({
|
|
523
|
-
req: Readable.fromWeb(req.body),
|
|
524
|
-
info,
|
|
525
|
-
headers: headersAsRecord,
|
|
526
|
-
id,
|
|
527
|
-
});
|
|
528
|
-
this.log('info', 'File upload complete', id);
|
|
529
|
-
return new Response(JSON.stringify({ success: true }), {
|
|
530
|
-
status: 200,
|
|
531
|
-
headers: {
|
|
532
|
-
'Content-Type': 'application/json',
|
|
533
|
-
},
|
|
534
|
-
});
|
|
535
|
-
} else if (req.method === 'GET') {
|
|
536
|
-
const data = await this.getFileData(info.libraryId, id);
|
|
537
|
-
return new Response(JSON.stringify(data), {
|
|
538
|
-
status: 200,
|
|
539
|
-
headers: {
|
|
540
|
-
'Content-Type': 'application/json',
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
|
-
} else {
|
|
544
|
-
throw new VerdantError(VerdantError.Code.InvalidRequest);
|
|
545
|
-
}
|
|
546
|
-
} catch (e) {
|
|
547
|
-
return this.getErrorResponse(e);
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
private getFileStorageOrThrow = () => {
|
|
552
|
-
if (!this.fileStorage) {
|
|
553
|
-
throw new VerdantError(VerdantError.Code.NoFileStorage);
|
|
554
|
-
}
|
|
555
|
-
return this.fileStorage;
|
|
556
|
-
};
|
|
557
|
-
|
|
558
|
-
private streamIncomingFile = ({
|
|
559
|
-
id,
|
|
560
|
-
req,
|
|
561
|
-
headers,
|
|
562
|
-
info,
|
|
563
|
-
}: {
|
|
564
|
-
req: Readable;
|
|
565
|
-
headers: Record<string, string | string[] | undefined>;
|
|
566
|
-
id: string;
|
|
567
|
-
info: TokenInfo;
|
|
568
|
-
}) => {
|
|
569
|
-
const fs = this.getFileStorageOrThrow();
|
|
570
|
-
|
|
571
|
-
// TODO: remove this once I figured out why the heck multipart
|
|
572
|
-
// file uploads from the integration testing environment are
|
|
573
|
-
// just completely hanging on request...
|
|
574
|
-
if (this.__testMode) {
|
|
575
|
-
this.log(
|
|
576
|
-
'warn',
|
|
577
|
-
'TEST MODE: FILE WILL NOT BE WRITTEN',
|
|
578
|
-
'\n',
|
|
579
|
-
'This Verdant server was launched in Test Mode. This is only meant for use',
|
|
580
|
-
'in automated testing environments. The file just uploaded WILL NOT be',
|
|
581
|
-
'stored on disk or retrievable.',
|
|
582
|
-
);
|
|
583
|
-
// Fake a file upload for testing
|
|
584
|
-
return this.storage.ready
|
|
585
|
-
.then(() => {
|
|
586
|
-
return this.storage.fileMetadata.put(info.libraryId, {
|
|
587
|
-
id,
|
|
588
|
-
libraryId: info.libraryId,
|
|
589
|
-
fileName: 'test.txt',
|
|
590
|
-
type: 'text/plain',
|
|
591
|
-
});
|
|
592
|
-
})
|
|
593
|
-
.then(() => {
|
|
594
|
-
return fs.put(
|
|
595
|
-
new Readable({
|
|
596
|
-
read() {
|
|
597
|
-
this.push('test');
|
|
598
|
-
this.push(null);
|
|
599
|
-
},
|
|
600
|
-
}),
|
|
601
|
-
{
|
|
602
|
-
id,
|
|
603
|
-
libraryId: info.libraryId,
|
|
604
|
-
fileName: 'test.txt',
|
|
605
|
-
type: 'text/plain',
|
|
606
|
-
},
|
|
607
|
-
);
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
return new Promise<void>((resolve, reject) => {
|
|
612
|
-
const bb = new Busboy({
|
|
613
|
-
headers: {
|
|
614
|
-
...headers,
|
|
615
|
-
'content-type': headers['content-type'] as string,
|
|
616
|
-
},
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
bb.on('file', async (fieldName, stream, fileName, _, mimeType) => {
|
|
620
|
-
// too many 'info's....
|
|
621
|
-
const lofiFileInfo: FileInfo = {
|
|
622
|
-
id,
|
|
623
|
-
libraryId: info.libraryId,
|
|
624
|
-
fileName,
|
|
625
|
-
type: mimeType,
|
|
626
|
-
};
|
|
627
|
-
// write metadata to storage
|
|
628
|
-
try {
|
|
629
|
-
await this.storage.ready;
|
|
630
|
-
await this.storage.fileMetadata.put(info.libraryId, lofiFileInfo);
|
|
631
|
-
fs.put(stream, lofiFileInfo);
|
|
632
|
-
this.log('info', 'File uploading', lofiFileInfo);
|
|
633
|
-
} catch (e) {
|
|
634
|
-
reject(e);
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
bb.on('finish', () => {
|
|
639
|
-
this.log('info', 'File upload stream finished');
|
|
640
|
-
resolve();
|
|
641
|
-
});
|
|
642
|
-
bb.on('error', (err) => {
|
|
643
|
-
this.log('error', 'File upload stream error', err);
|
|
644
|
-
reject(new VerdantError(VerdantError.Code.Unexpected, err as any));
|
|
645
|
-
});
|
|
646
|
-
bb.on('fieldsLimit', () => {
|
|
647
|
-
reject(
|
|
648
|
-
new VerdantError(
|
|
649
|
-
VerdantError.Code.Unexpected,
|
|
650
|
-
undefined,
|
|
651
|
-
'Fields limit exceeded',
|
|
652
|
-
),
|
|
653
|
-
);
|
|
654
|
-
});
|
|
655
|
-
bb.on('filesLimit', () => {
|
|
656
|
-
reject(
|
|
657
|
-
new VerdantError(
|
|
658
|
-
VerdantError.Code.Unexpected,
|
|
659
|
-
undefined,
|
|
660
|
-
'Files limit exceeded',
|
|
661
|
-
),
|
|
662
|
-
);
|
|
663
|
-
});
|
|
664
|
-
bb.on('partsLimit', () => {
|
|
665
|
-
reject(
|
|
666
|
-
new VerdantError(
|
|
667
|
-
VerdantError.Code.Unexpected,
|
|
668
|
-
undefined,
|
|
669
|
-
'Parts limit exceeded',
|
|
670
|
-
),
|
|
671
|
-
);
|
|
672
|
-
});
|
|
673
|
-
req.pipe(bb);
|
|
674
|
-
});
|
|
675
|
-
};
|
|
676
|
-
|
|
677
|
-
getFileData = async (library: string, fileId: string): Promise<FileData> => {
|
|
678
|
-
const fs = this.getFileStorageOrThrow();
|
|
679
|
-
|
|
680
|
-
await this.storage.ready;
|
|
681
|
-
const fileInfo = await this.storage.fileMetadata.get(library, fileId);
|
|
682
|
-
if (!fileInfo) {
|
|
683
|
-
throw new VerdantError(
|
|
684
|
-
VerdantError.Code.NotFound,
|
|
685
|
-
undefined,
|
|
686
|
-
`File ${fileId} not found`,
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const url = await fs.getUrl({
|
|
691
|
-
fileName: fileInfo.name,
|
|
692
|
-
id: fileInfo.fileId,
|
|
693
|
-
libraryId: library,
|
|
694
|
-
type: fileInfo.type,
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
return {
|
|
698
|
-
id: fileInfo.fileId,
|
|
699
|
-
url,
|
|
700
|
-
remote: true,
|
|
701
|
-
name: fileInfo.name,
|
|
702
|
-
type: fileInfo.type,
|
|
703
|
-
};
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
broadcast = (
|
|
707
|
-
libraryId: string,
|
|
708
|
-
message: ServerMessage,
|
|
709
|
-
omitKeys: string[] = [],
|
|
710
|
-
) => {
|
|
711
|
-
this.clientConnections.broadcast(libraryId, message, omitKeys);
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
send = (libraryId: string, key: string, message: ServerMessage) => {
|
|
715
|
-
this.clientConnections.respond(libraryId, key, message);
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
private handleMessage = (
|
|
719
|
-
clientKey: string,
|
|
720
|
-
info: TokenInfo,
|
|
721
|
-
message: ClientMessage,
|
|
722
|
-
) => {
|
|
723
|
-
this.log(
|
|
724
|
-
'debug',
|
|
725
|
-
'Got message from user',
|
|
726
|
-
info.userId,
|
|
727
|
-
', library',
|
|
728
|
-
info.libraryId,
|
|
729
|
-
JSON.stringify(message),
|
|
730
|
-
);
|
|
731
|
-
return this.library.receive(message, clientKey, info);
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
private handleConnection = (
|
|
735
|
-
ws: WebSocket,
|
|
736
|
-
req: IncomingMessage,
|
|
737
|
-
info: TokenInfo,
|
|
738
|
-
) => {
|
|
739
|
-
const key = generateId();
|
|
740
|
-
|
|
741
|
-
this.clientConnections.addSocket(info.libraryId, key, ws, info);
|
|
742
|
-
|
|
743
|
-
ws.on('message', async (message: any) => {
|
|
744
|
-
const data = JSON.parse(message.toString()) as ClientMessage;
|
|
745
|
-
|
|
746
|
-
if (data.replicaId) {
|
|
747
|
-
this.connectionToReplicaIdMap.set(ws, data.replicaId);
|
|
748
|
-
this.keepalives.refresh(info.libraryId, data.replicaId);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
await this.handleMessage(key, info, data);
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
ws.on('close', () => {
|
|
755
|
-
const replicaId = this.connectionToReplicaIdMap.get(ws);
|
|
756
|
-
if (!replicaId) {
|
|
757
|
-
this.emit('warning', 'Unknown replica disconnected', ws);
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
this.library.remove(info.libraryId, replicaId);
|
|
762
|
-
|
|
763
|
-
this.emit('socket-close', info);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
this.emit('socket-connection', info);
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
listen = (...params: Parameters<HttpServer['listen']>) => {
|
|
770
|
-
this.httpServer.listen(...params);
|
|
771
|
-
};
|
|
772
|
-
|
|
773
|
-
close = async () => {
|
|
774
|
-
await Promise.all([
|
|
775
|
-
new Promise<void>((resolve) => {
|
|
776
|
-
setTimeout(() => {
|
|
777
|
-
this.log('warn', 'HTTP server close timed out');
|
|
778
|
-
resolve();
|
|
779
|
-
}, 5 * 1000);
|
|
780
|
-
this.httpServer.close(() => {
|
|
781
|
-
resolve();
|
|
782
|
-
this.log('info', 'HTTP server closed');
|
|
783
|
-
});
|
|
784
|
-
}),
|
|
785
|
-
new Promise<void>((resolve) => {
|
|
786
|
-
setTimeout(() => {
|
|
787
|
-
this.log('warn', 'Socket server close timed out');
|
|
788
|
-
resolve();
|
|
789
|
-
}, 5 * 1000);
|
|
790
|
-
this.wss.close(() => {
|
|
791
|
-
resolve();
|
|
792
|
-
this.log('info', 'Socket server closed');
|
|
793
|
-
});
|
|
794
|
-
}),
|
|
795
|
-
]);
|
|
796
|
-
await this.storage.close();
|
|
797
|
-
};
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* Completely remove a library from the server.
|
|
801
|
-
* This will remove all data associated with the library.
|
|
802
|
-
* Use it very carefully! Data will still be stored on user
|
|
803
|
-
* devices, so this is not as scary as it sounds - the next time
|
|
804
|
-
* any replica connects, it will repopulate the library. But
|
|
805
|
-
* you should still be careful.
|
|
806
|
-
*/
|
|
807
|
-
evictLibrary = async (libraryId: string) => {
|
|
808
|
-
// disconnect all clients
|
|
809
|
-
this.clientConnections.disconnectAll(libraryId);
|
|
810
|
-
await this.library.destroy(libraryId);
|
|
811
|
-
this.log('info', 'Evicted library', libraryId);
|
|
812
|
-
};
|
|
813
|
-
|
|
814
|
-
/**
|
|
815
|
-
* Returns currently connected replica info for a library
|
|
816
|
-
*/
|
|
817
|
-
getLibraryPresence = (libraryId: string) => {
|
|
818
|
-
return this.library.getPresence(libraryId);
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
/**
|
|
822
|
-
* Returns helpful information about a library.
|
|
823
|
-
* This is currently only the known replicas and some
|
|
824
|
-
* more low-level metadata.
|
|
825
|
-
*/
|
|
826
|
-
getLibraryInfo = (libraryId: string) => {
|
|
827
|
-
return this.library.getInfo(libraryId);
|
|
828
|
-
};
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
* Removes all replicas associated with a User ID from
|
|
832
|
-
* the server. Consistency algorithms will no longer wait on these
|
|
833
|
-
* replicas before confirming and squashing changes.
|
|
834
|
-
*/
|
|
835
|
-
evictUser = (libraryId: string, userId: string) => {
|
|
836
|
-
this.library.evictUser(libraryId, userId);
|
|
837
|
-
this.log('info', 'Evicted user', userId, 'from library', libraryId);
|
|
838
|
-
};
|
|
839
|
-
|
|
840
|
-
/**
|
|
841
|
-
* Retrieves a snapshot of the full document contents at this time.
|
|
842
|
-
* Useful for capturing the shape of data for static usage outside
|
|
843
|
-
* of the live system.
|
|
844
|
-
*/
|
|
845
|
-
getDocumentSnapshot = (
|
|
846
|
-
libraryId: string,
|
|
847
|
-
collection: string,
|
|
848
|
-
documentId: string,
|
|
849
|
-
) => {
|
|
850
|
-
return this.library.getDocumentSnapshot(
|
|
851
|
-
libraryId,
|
|
852
|
-
createOid(collection, documentId),
|
|
853
|
-
);
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
function isFetch(request: Request | IncomingMessage): request is Request {
|
|
858
|
-
return 'json' in request;
|
|
859
|
-
}
|