@verdant-web/server 3.3.11 → 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.
Files changed (204) hide show
  1. package/bin/server.mjs +110 -107
  2. package/dist/esm/TokenVerifier.d.ts +1 -0
  3. package/dist/esm/TokenVerifier.js +3 -0
  4. package/dist/esm/TokenVerifier.js.map +1 -1
  5. package/dist/esm/adapters/node/errorHandler.d.ts +2 -0
  6. package/dist/esm/adapters/node/errorHandler.js +12 -0
  7. package/dist/esm/adapters/node/errorHandler.js.map +1 -0
  8. package/dist/esm/adapters/node/getRequestToken.d.ts +1 -0
  9. package/dist/esm/adapters/node/getRequestToken.js +25 -0
  10. package/dist/esm/adapters/node/getRequestToken.js.map +1 -0
  11. package/dist/esm/adapters/node/httpRouter.d.ts +47 -0
  12. package/dist/esm/adapters/node/httpRouter.js +88 -0
  13. package/dist/esm/adapters/node/httpRouter.js.map +1 -0
  14. package/dist/esm/adapters/node/index.d.ts +5 -0
  15. package/dist/esm/adapters/node/index.js +6 -0
  16. package/dist/esm/adapters/node/index.js.map +1 -0
  17. package/dist/esm/adapters/node/tokenMiddleware.d.ts +7 -0
  18. package/dist/esm/adapters/node/tokenMiddleware.js +22 -0
  19. package/dist/esm/adapters/node/tokenMiddleware.js.map +1 -0
  20. package/dist/esm/adapters/node/websocketHandler.d.ts +4 -0
  21. package/dist/esm/adapters/node/websocketHandler.js +54 -0
  22. package/dist/esm/adapters/node/websocketHandler.js.map +1 -0
  23. package/dist/esm/connections/ClientConnection.d.ts +25 -0
  24. package/dist/esm/{ClientConnection.js → connections/ClientConnection.js} +47 -38
  25. package/dist/esm/connections/ClientConnection.js.map +1 -0
  26. package/dist/esm/connections/MessageSender.d.ts +5 -0
  27. package/dist/esm/connections/MessageSender.js.map +1 -0
  28. package/dist/esm/connections/Presence.d.ts +23 -0
  29. package/dist/esm/connections/Presence.js +65 -0
  30. package/dist/esm/connections/Presence.js.map +1 -0
  31. package/dist/esm/core.d.ts +15 -0
  32. package/dist/esm/core.js +15 -0
  33. package/dist/esm/core.js.map +1 -0
  34. package/dist/esm/defaults.d.ts +2 -0
  35. package/dist/esm/defaults.js +3 -0
  36. package/dist/esm/defaults.js.map +1 -0
  37. package/dist/esm/files/FileStorage.d.ts +12 -3
  38. package/dist/esm/files/FileStorage.js +20 -0
  39. package/dist/esm/files/FileStorage.js.map +1 -1
  40. package/dist/esm/index.d.ts +7 -7
  41. package/dist/esm/index.js +2 -2
  42. package/dist/esm/index.js.map +1 -1
  43. package/dist/esm/internals.d.ts +14 -0
  44. package/dist/esm/internals.js +12 -0
  45. package/dist/esm/internals.js.map +1 -0
  46. package/dist/esm/libraries/Library.d.ts +78 -0
  47. package/dist/esm/{ServerLibrary.js → libraries/Library.js} +153 -144
  48. package/dist/esm/libraries/Library.js.map +1 -0
  49. package/dist/esm/{ServerLibrary.test.js → libraries/Library.test.js} +42 -33
  50. package/dist/esm/libraries/Library.test.js.map +1 -0
  51. package/dist/esm/libraries/generic.d.ts +8 -0
  52. package/dist/esm/libraries/generic.js +2 -0
  53. package/dist/esm/libraries/generic.js.map +1 -0
  54. package/dist/esm/libraries/singleNode.d.ts +53 -0
  55. package/dist/esm/libraries/singleNode.js +88 -0
  56. package/dist/esm/libraries/singleNode.js.map +1 -0
  57. package/dist/esm/logger.d.ts +1 -0
  58. package/dist/esm/logger.js +2 -0
  59. package/dist/esm/logger.js.map +1 -0
  60. package/dist/esm/storage/Storage.d.ts +39 -38
  61. package/dist/esm/storage/StorageManager.d.ts +11 -0
  62. package/dist/esm/storage/StorageManager.js +32 -0
  63. package/dist/esm/storage/StorageManager.js.map +1 -0
  64. package/dist/esm/storage/index.d.ts +1 -2
  65. package/dist/esm/storage/index.js +1 -2
  66. package/dist/esm/storage/index.js.map +1 -1
  67. package/dist/esm/storage/sqlShard/Databases.d.ts +5 -3
  68. package/dist/esm/storage/sqlShard/Databases.js +11 -7
  69. package/dist/esm/storage/sqlShard/Databases.js.map +1 -1
  70. package/dist/esm/storage/sqlShard/SqlBaselines.d.ts +16 -17
  71. package/dist/esm/storage/sqlShard/SqlBaselines.js +55 -59
  72. package/dist/esm/storage/sqlShard/SqlBaselines.js.map +1 -1
  73. package/dist/esm/storage/sqlShard/SqlFileMetadata.d.ts +13 -13
  74. package/dist/esm/storage/sqlShard/SqlFileMetadata.js +44 -57
  75. package/dist/esm/storage/sqlShard/SqlFileMetadata.js.map +1 -1
  76. package/dist/esm/storage/sqlShard/SqlOperations.d.ts +12 -12
  77. package/dist/esm/storage/sqlShard/SqlOperations.js +38 -73
  78. package/dist/esm/storage/sqlShard/SqlOperations.js.map +1 -1
  79. package/dist/esm/storage/sqlShard/SqlReplicas.d.ts +18 -17
  80. package/dist/esm/storage/sqlShard/SqlReplicas.js +63 -100
  81. package/dist/esm/storage/sqlShard/SqlReplicas.js.map +1 -1
  82. package/dist/esm/storage/sqlShard/database.d.ts +25 -5
  83. package/dist/esm/storage/sqlShard/database.js +47 -15
  84. package/dist/esm/storage/sqlShard/database.js.map +1 -1
  85. package/dist/esm/storage/sqlShard/migrations/v1.d.ts +2 -3
  86. package/dist/esm/storage/sqlShard/migrations/v1.js +42 -47
  87. package/dist/esm/storage/sqlShard/migrations/v1.js.map +1 -1
  88. package/dist/esm/storage/sqlShard/migrations/v2.d.ts +2 -3
  89. package/dist/esm/storage/sqlShard/migrations/v2.js +5 -15
  90. package/dist/esm/storage/sqlShard/migrations/v2.js.map +1 -1
  91. package/dist/esm/storage/sqlShard/migrations.d.ts +5 -7
  92. package/dist/esm/storage/sqlShard/migrations.js +48 -1
  93. package/dist/esm/storage/sqlShard/migrations.js.map +1 -1
  94. package/dist/esm/storage/sqlShard/sqlShardStorage.d.ts +25 -4
  95. package/dist/esm/storage/sqlShard/sqlShardStorage.js +43 -40
  96. package/dist/esm/storage/sqlShard/sqlShardStorage.js.map +1 -1
  97. package/dist/esm/storage/sqlShard/tables.d.ts +9 -6
  98. package/dist/esm/types.d.ts +1 -2
  99. package/package.json +24 -13
  100. package/src/TokenVerifier.ts +4 -0
  101. package/src/adapters/node/errorHandler.ts +18 -0
  102. package/src/adapters/node/getRequestToken.ts +34 -0
  103. package/src/adapters/node/httpRouter.ts +139 -0
  104. package/src/adapters/node/index.ts +5 -0
  105. package/src/adapters/node/tokenMiddleware.ts +32 -0
  106. package/src/adapters/node/websocketHandler.ts +79 -0
  107. package/src/{ClientConnection.ts → connections/ClientConnection.ts} +64 -57
  108. package/src/connections/MessageSender.ts +6 -0
  109. package/src/connections/Presence.ts +95 -0
  110. package/src/core.ts +36 -0
  111. package/src/defaults.ts +2 -0
  112. package/src/files/FileStorage.ts +36 -3
  113. package/src/index.ts +7 -7
  114. package/src/internals.ts +14 -0
  115. package/src/{ServerLibrary.test.ts → libraries/Library.test.ts} +51 -49
  116. package/src/{ServerLibrary.ts → libraries/Library.ts} +203 -206
  117. package/src/libraries/generic.ts +9 -0
  118. package/src/libraries/singleNode.ts +145 -0
  119. package/src/logger.ts +4 -0
  120. package/src/storage/Storage.ts +37 -66
  121. package/src/storage/StorageManager.ts +39 -0
  122. package/src/storage/index.ts +1 -2
  123. package/src/storage/sqlShard/Databases.ts +26 -13
  124. package/src/storage/sqlShard/SqlBaselines.ts +58 -67
  125. package/src/storage/sqlShard/SqlFileMetadata.ts +51 -72
  126. package/src/storage/sqlShard/SqlOperations.ts +57 -81
  127. package/src/storage/sqlShard/SqlReplicas.ts +80 -121
  128. package/src/storage/sqlShard/database.ts +82 -18
  129. package/src/storage/sqlShard/migrations/v1.ts +42 -52
  130. package/src/storage/sqlShard/migrations/v2.ts +5 -19
  131. package/src/storage/sqlShard/migrations.ts +66 -1
  132. package/src/storage/sqlShard/sqlShardStorage.ts +56 -55
  133. package/src/storage/sqlShard/tables.ts +6 -6
  134. package/src/types.ts +1 -2
  135. package/dist/esm/ClientConnection.d.ts +0 -19
  136. package/dist/esm/ClientConnection.js.map +0 -1
  137. package/dist/esm/MessageSender.d.ts +0 -5
  138. package/dist/esm/MessageSender.js.map +0 -1
  139. package/dist/esm/Presence.d.ts +0 -18
  140. package/dist/esm/Presence.js +0 -65
  141. package/dist/esm/Presence.js.map +0 -1
  142. package/dist/esm/ReplicaKeepaliveTimers.d.ts +0 -17
  143. package/dist/esm/ReplicaKeepaliveTimers.js +0 -27
  144. package/dist/esm/ReplicaKeepaliveTimers.js.map +0 -1
  145. package/dist/esm/Server.d.ts +0 -166
  146. package/dist/esm/Server.js +0 -572
  147. package/dist/esm/Server.js.map +0 -1
  148. package/dist/esm/ServerLibrary.d.ts +0 -64
  149. package/dist/esm/ServerLibrary.js.map +0 -1
  150. package/dist/esm/ServerLibrary.test.js.map +0 -1
  151. package/dist/esm/storage/sql/SqlBaselines.d.ts +0 -22
  152. package/dist/esm/storage/sql/SqlBaselines.js +0 -107
  153. package/dist/esm/storage/sql/SqlBaselines.js.map +0 -1
  154. package/dist/esm/storage/sql/SqlFileMetadata.d.ts +0 -18
  155. package/dist/esm/storage/sql/SqlFileMetadata.js +0 -69
  156. package/dist/esm/storage/sql/SqlFileMetadata.js.map +0 -1
  157. package/dist/esm/storage/sql/SqlOperations.d.ts +0 -19
  158. package/dist/esm/storage/sql/SqlOperations.js +0 -122
  159. package/dist/esm/storage/sql/SqlOperations.js.map +0 -1
  160. package/dist/esm/storage/sql/SqlReplicas.d.ts +0 -32
  161. package/dist/esm/storage/sql/SqlReplicas.js +0 -163
  162. package/dist/esm/storage/sql/SqlReplicas.js.map +0 -1
  163. package/dist/esm/storage/sql/database.d.ts +0 -9
  164. package/dist/esm/storage/sql/database.js +0 -24
  165. package/dist/esm/storage/sql/database.js.map +0 -1
  166. package/dist/esm/storage/sql/migrations/v1.d.ts +0 -3
  167. package/dist/esm/storage/sql/migrations/v1.js +0 -59
  168. package/dist/esm/storage/sql/migrations/v1.js.map +0 -1
  169. package/dist/esm/storage/sql/migrations/v2.d.ts +0 -3
  170. package/dist/esm/storage/sql/migrations/v2.js +0 -16
  171. package/dist/esm/storage/sql/migrations/v2.js.map +0 -1
  172. package/dist/esm/storage/sql/migrations.d.ts +0 -7
  173. package/dist/esm/storage/sql/migrations.js +0 -4
  174. package/dist/esm/storage/sql/migrations.js.map +0 -1
  175. package/dist/esm/storage/sql/sqlStorage.d.ts +0 -6
  176. package/dist/esm/storage/sql/sqlStorage.js +0 -35
  177. package/dist/esm/storage/sql/sqlStorage.js.map +0 -1
  178. package/dist/esm/storage/sql/tables.d.ts +0 -44
  179. package/dist/esm/storage/sql/tables.js +0 -2
  180. package/dist/esm/storage/sql/tables.js.map +0 -1
  181. package/dist/esm/storage/sqlShard/transfer.d.ts +0 -7
  182. package/dist/esm/storage/sqlShard/transfer.js +0 -90
  183. package/dist/esm/storage/sqlShard/transfer.js.map +0 -1
  184. package/dist/esm/storage/sqlShard/transfer.test.d.ts +0 -1
  185. package/dist/esm/storage/sqlShard/transfer.test.js +0 -109
  186. package/dist/esm/storage/sqlShard/transfer.test.js.map +0 -1
  187. package/src/MessageSender.ts +0 -10
  188. package/src/Presence.ts +0 -86
  189. package/src/ReplicaKeepaliveTimers.ts +0 -34
  190. package/src/Server.ts +0 -859
  191. package/src/storage/sql/SqlBaselines.ts +0 -147
  192. package/src/storage/sql/SqlFileMetadata.ts +0 -94
  193. package/src/storage/sql/SqlOperations.ts +0 -160
  194. package/src/storage/sql/SqlReplicas.ts +0 -234
  195. package/src/storage/sql/database.ts +0 -32
  196. package/src/storage/sql/migrations/v1.ts +0 -64
  197. package/src/storage/sql/migrations/v2.ts +0 -20
  198. package/src/storage/sql/migrations.ts +0 -4
  199. package/src/storage/sql/sqlStorage.ts +0 -57
  200. package/src/storage/sql/tables.ts +0 -51
  201. package/src/storage/sqlShard/transfer.test.ts +0 -149
  202. package/src/storage/sqlShard/transfer.ts +0 -123
  203. /package/dist/esm/{MessageSender.js → connections/MessageSender.js} +0 -0
  204. /package/dist/esm/{ServerLibrary.test.d.ts → libraries/Library.test.d.ts} +0 -0
@@ -0,0 +1,139 @@
1
+ import { ClientMessage, VerdantError } from '@verdant-web/common';
2
+ import { Hono } from 'hono';
3
+ import { Readable } from 'stream';
4
+ import { SingleNodeLibraryManager } from '../../libraries/singleNode.js';
5
+ import { errorHandler } from './errorHandler.js';
6
+ import { tokenMiddleware } from './tokenMiddleware.js';
7
+
8
+ export function createHttpRouter(core: SingleNodeLibraryManager) {
9
+ const mw = tokenMiddleware(core.tokenVerifier);
10
+ const app = new Hono<any>()
11
+ .onError(errorHandler)
12
+ .use('*', async (c, next) => {
13
+ core.log('debug', 'Incoming request', {
14
+ method: c.req.method,
15
+ path: c.req.path,
16
+ });
17
+ await next();
18
+ })
19
+ .post('/', mw, async (ctx) => {
20
+ const key = ctx.get('key');
21
+ const info = ctx.get('tokenInfo');
22
+
23
+ const microserver = await core.get(info.libraryId);
24
+
25
+ const finish = microserver.onRequest?.(key, ctx.req.raw, info);
26
+
27
+ const body = (await ctx.req.json()) as
28
+ | { messages: ClientMessage[] }
29
+ | null
30
+ | undefined;
31
+
32
+ if (!body?.messages) {
33
+ throw new VerdantError(
34
+ VerdantError.Code.BodyRequired,
35
+ undefined,
36
+ 'Invalid request body',
37
+ );
38
+ }
39
+
40
+ for (const message of body.messages) {
41
+ await microserver.handleMessage(key, info, message);
42
+ }
43
+
44
+ const res = finish();
45
+
46
+ return res;
47
+ })
48
+ .post('/files/:fileId', mw, async (ctx) => {
49
+ const info = ctx.get('tokenInfo');
50
+
51
+ const id = ctx.req.param('fileId');
52
+ if (!id) {
53
+ throw new VerdantError(
54
+ VerdantError.Code.NotFound,
55
+ undefined,
56
+ 'Supply a file ID as a path parameters',
57
+ );
58
+ }
59
+
60
+ // TODO: remove this once I figure out why request file body parsing is
61
+ // hanging indefinitely.
62
+ if (core.__testMode) {
63
+ const microserver = await core.get(info.libraryId);
64
+
65
+ core.log(
66
+ 'warn',
67
+ 'TEST MODE: FILE WILL NOT BE WRITTEN',
68
+ '\n',
69
+ 'This Verdant server was launched in Test Mode. This is only meant for use',
70
+ 'in automated testing environments. The file just uploaded WILL NOT be',
71
+ 'stored on disk or retrievable.',
72
+ );
73
+ // Fake a file upload for testing
74
+ await microserver.uploadFile(
75
+ new Readable({
76
+ read() {
77
+ this.push('test');
78
+ this.push(null);
79
+ },
80
+ }),
81
+ {
82
+ id,
83
+ libraryId: info.libraryId,
84
+ fileName: 'test.txt',
85
+ type: 'text/plain',
86
+ },
87
+ );
88
+ return ctx.json({ success: true }, 200);
89
+ }
90
+
91
+ const form = await ctx.req.parseBody();
92
+ const file = form.file;
93
+ if (!file || !(file instanceof File)) {
94
+ throw new VerdantError(
95
+ VerdantError.Code.InvalidRequest,
96
+ undefined,
97
+ 'No file was uploaded to the `file` form key',
98
+ );
99
+ }
100
+
101
+ const microserver = await core.get(info.libraryId);
102
+
103
+ const fileStream = file.stream();
104
+ await microserver.uploadFile(fileStream, {
105
+ id,
106
+ libraryId: info.libraryId,
107
+ fileName: file.name,
108
+ type: file.type,
109
+ });
110
+
111
+ return ctx.json({ success: true }, 200);
112
+ })
113
+ .get('/files/:fileId', mw, async (ctx) => {
114
+ const info = ctx.get('tokenInfo');
115
+
116
+ const id = ctx.req.param('fileId');
117
+ if (!id) {
118
+ throw new VerdantError(
119
+ VerdantError.Code.NotFound,
120
+ undefined,
121
+ 'Supply a file ID as a path parameters',
122
+ );
123
+ }
124
+
125
+ const microserver = await core.get(info.libraryId);
126
+
127
+ const file = await microserver.getFileInfo(id);
128
+ if (!file) {
129
+ throw new VerdantError(
130
+ VerdantError.Code.NotFound,
131
+ undefined,
132
+ `File with ID ${id} not found`,
133
+ );
134
+ }
135
+
136
+ return ctx.json(file, 200);
137
+ });
138
+ return app;
139
+ }
@@ -0,0 +1,5 @@
1
+ export { errorHandler } from '../node/errorHandler.js';
2
+ export { getRequestToken } from '../node/getRequestToken.js';
3
+ export * from './httpRouter.js';
4
+ export { tokenMiddleware } from './tokenMiddleware.js';
5
+ export * from './websocketHandler.js';
@@ -0,0 +1,32 @@
1
+ import { generateId } from '@verdant-web/common';
2
+ import { createMiddleware } from 'hono/factory';
3
+ import { TokenInfo, TokenVerifier } from '../../TokenVerifier.js';
4
+ import { getRequestToken } from '../node/getRequestToken.js';
5
+
6
+ export const tokenMiddleware = (tokenVerifier: TokenVerifier) =>
7
+ createMiddleware<{
8
+ Variables: {
9
+ key: string;
10
+ tokenInfo: TokenInfo;
11
+ };
12
+ }>((ctx, next) => {
13
+ if (ctx.req.method === 'OPTIONS') {
14
+ // preflight request, np
15
+ return next();
16
+ }
17
+ const requestToken = getRequestToken(ctx.req.raw)!;
18
+
19
+ const key = generateId();
20
+ const info = tokenVerifier.verifyToken(requestToken);
21
+ ctx.set('key', key);
22
+ ctx.set('tokenInfo', info);
23
+
24
+ // if a Sec-Websocket-Protocol header was sent, we need to echo it back
25
+ // so the websocket connection is accepted by the client
26
+ const protocolHeader = ctx.req.header('Sec-WebSocket-Protocol');
27
+ if (protocolHeader) {
28
+ ctx.res.headers.set('Sec-WebSocket-Protocol', requestToken);
29
+ }
30
+
31
+ return next();
32
+ });
@@ -0,0 +1,79 @@
1
+ import {
2
+ ClientMessage,
3
+ VerdantError,
4
+ assert,
5
+ generateId,
6
+ } from '@verdant-web/common';
7
+ import { IncomingMessage } from 'node:http';
8
+ import internal from 'node:stream';
9
+ import { URL } from 'url';
10
+ import { WebSocket, WebSocketServer } from 'ws';
11
+ import { TokenInfo } from '../../TokenVerifier.js';
12
+ import { SingleNodeLibraryManager } from '../../libraries/singleNode.js';
13
+
14
+ export function createNodeWebsocketHandler(core: SingleNodeLibraryManager) {
15
+ const wss = new WebSocketServer({ noServer: true });
16
+ wss.on(
17
+ 'connection',
18
+ async (ws: WebSocket, req: IncomingMessage, info: TokenInfo) => {
19
+ const key = generateId();
20
+
21
+ const microserver = await core.get(info.libraryId);
22
+
23
+ microserver.clientConnections.addSocket(key, ws, info);
24
+
25
+ ws.on('message', async (message: any) => {
26
+ const data = JSON.parse(message.toString()) as ClientMessage;
27
+ await microserver.handleMessage(key, info, data);
28
+ });
29
+
30
+ ws.on('close', () => {
31
+ microserver.clientConnections.remove(key);
32
+ core.log('debug', 'Connection closed', { key, userId: info.userId });
33
+ });
34
+ },
35
+ );
36
+
37
+ function getRequestToken(req: IncomingMessage) {
38
+ if (req.headers['sec-websocket-protocol']) {
39
+ // hack: read the token from the websocket protocol header
40
+ const [type, token] = req.headers['sec-websocket-protocol'].split(',');
41
+ if (type === 'Bearer') {
42
+ return token.trim();
43
+ }
44
+ }
45
+
46
+ assert(req.url, 'Request URL is required');
47
+ const url = new URL(req.url, 'http://localhost');
48
+ const token = url.searchParams.get('token');
49
+ if (!token) {
50
+ throw new VerdantError(
51
+ VerdantError.Code.NoToken,
52
+ undefined,
53
+ 'Token query parameter is required',
54
+ );
55
+ }
56
+
57
+ return token;
58
+ }
59
+
60
+ return (req: IncomingMessage, socket: internal.Duplex, head: Buffer) => {
61
+ try {
62
+ const token = getRequestToken(req);
63
+ const info = core.tokenVerifier.verifyToken(token);
64
+ wss.handleUpgrade(req, socket, head, (ws) => {
65
+ wss.emit('connection', ws, req, info);
66
+ });
67
+ } catch (err) {
68
+ if (err instanceof VerdantError && err.httpStatus === 401) {
69
+ socket.write(
70
+ 'HTTP/1.1 401 Unauthorized\r\n' +
71
+ 'Connection: close\r\n' +
72
+ 'Content-Length: 0\r\n' +
73
+ '\r\n',
74
+ );
75
+ }
76
+ socket.destroy();
77
+ }
78
+ };
79
+ }
@@ -1,7 +1,10 @@
1
- import { authz, Operation, ServerMessage } from '@verdant-web/common';
1
+ import { authz, ServerMessage } from '@verdant-web/common';
2
2
  import { IncomingMessage, ServerResponse } from 'http';
3
- import { WebSocket } from 'ws';
4
- import { TokenInfo } from './TokenVerifier.js';
3
+ import { WebSocket as WSWebSocket } from 'ws';
4
+ import { UserProfileLoader } from '../Profiles.js';
5
+ import { TokenInfo } from '../TokenVerifier.js';
6
+ import { MessageSender } from './MessageSender.js';
7
+ import { Presence } from './Presence.js';
5
8
 
6
9
  abstract class ClientConnection {
7
10
  constructor(
@@ -109,59 +112,79 @@ class FetchClientConnection extends ClientConnection {
109
112
  };
110
113
  }
111
114
 
115
+ function subscribeToSocketEvent(
116
+ socket: WSWebSocket | WebSocket,
117
+ event: string,
118
+ handler: (data: any) => void,
119
+ ) {
120
+ if ('on' in socket) {
121
+ (socket as WSWebSocket).on(event as any, handler);
122
+ return () => (socket as WSWebSocket).off(event as any, handler);
123
+ } else {
124
+ (socket as WebSocket).addEventListener(event as any, (evt) =>
125
+ handler((evt as any).data),
126
+ );
127
+ return () =>
128
+ (socket as WebSocket).removeEventListener(event as any, handler);
129
+ }
130
+ }
131
+
112
132
  class WebSocketClientConnection extends ClientConnection {
113
133
  readonly type = 'websocket';
114
134
 
115
135
  constructor(
116
136
  key: string,
117
- readonly ws: WebSocket,
137
+ // supports either NodeJS+WS mock websocket or native WebSocket
138
+ // for Cloudflare / Edge environments
139
+ readonly ws: WebSocket | WSWebSocket,
118
140
  info: TokenInfo,
119
141
  ) {
120
142
  super(key, info);
121
143
  }
122
144
 
123
145
  handleMessage = (message: ServerMessage) => {
124
- this.ws.send(JSON.stringify(message));
146
+ return this.ws.send(JSON.stringify(message));
125
147
  };
126
148
  }
127
149
 
128
- export class ClientConnectionManager {
129
- private readonly connections: Record<string, Map<string, ClientConnection>> =
130
- {};
150
+ export class ClientConnectionManager implements MessageSender {
151
+ private readonly connections: Map<string, ClientConnection> = new Map();
152
+ readonly presence: Presence;
131
153
 
132
- private getLibraryConnections = (libraryId: string) => {
133
- if (!this.connections[libraryId]) {
134
- this.connections[libraryId] = new Map();
135
- }
136
- return this.connections[libraryId]!;
137
- };
154
+ constructor({ profiles }: { profiles: UserProfileLoader<any> }) {
155
+ this.presence = new Presence(profiles);
156
+ }
138
157
 
139
158
  addSocket = (
140
- libraryId: string,
141
159
  key: string,
142
- socket: WebSocket,
160
+ socket: WebSocket | WSWebSocket,
143
161
  info: TokenInfo,
144
162
  ) => {
145
- const lib = this.getLibraryConnections(libraryId);
146
-
147
163
  const connection = new WebSocketClientConnection(key, socket, info);
148
164
 
149
- lib.set(key, connection);
165
+ this.connections.set(key, connection);
150
166
 
151
- socket.on('close', () => {
152
- lib.delete(key);
153
- });
167
+ const unsubscribes: (() => void)[] = [];
168
+ unsubscribes.push(
169
+ subscribeToSocketEvent(socket, 'close', () => {
170
+ this.connections.delete(key);
171
+ this.presence.removeConnection(key);
172
+ unsubscribes.forEach((u) => u());
173
+ }),
174
+ );
175
+ unsubscribes.push(
176
+ subscribeToSocketEvent(socket, 'messsage', () => {
177
+ this.presence.keepAlive(key);
178
+ }),
179
+ );
154
180
  };
155
181
 
156
182
  addRequest = (
157
- libraryId: string,
158
183
  key: string,
159
184
  request: IncomingMessage,
160
185
  response: ServerResponse,
161
186
  info: TokenInfo,
162
187
  ) => {
163
- const lib = this.getLibraryConnections(libraryId);
164
-
165
188
  const connection = new RequestClientConnection(
166
189
  key,
167
190
  request,
@@ -169,47 +192,37 @@ export class ClientConnectionManager {
169
192
  info,
170
193
  );
171
194
 
172
- lib.set(key, connection);
195
+ this.connections.set(key, connection);
173
196
 
174
197
  return () => {
175
198
  connection.end();
176
- lib.delete(key);
199
+ this.connections.delete(key);
177
200
  };
178
201
  };
179
202
 
180
- addFetch = (
181
- libraryId: string,
182
- key: string,
183
- req: Request,
184
- info: TokenInfo,
185
- ): (() => Response) => {
186
- const lib = this.getLibraryConnections(libraryId);
187
-
203
+ addFetch = (key: string, req: Request, info: TokenInfo): (() => Response) => {
188
204
  const connection = new FetchClientConnection(key, req, info);
189
205
 
190
- lib.set(key, connection);
206
+ this.connections.set(key, connection);
191
207
 
192
208
  return () => {
193
209
  connection.end();
194
- lib.delete(key);
210
+ this.connections.delete(key);
195
211
  return connection.res;
196
212
  };
197
213
  };
198
214
 
199
- remove = (libraryId: string, key: string) => {
200
- const lib = this.getLibraryConnections(libraryId);
201
- lib.delete(key);
215
+ remove = (key: string) => {
216
+ this.connections.delete(key);
217
+ this.presence.removeConnection(key);
202
218
  };
203
219
 
204
- respond = (libraryId: string, key: string, message: ServerMessage) => {
205
- const lib = this.getLibraryConnections(libraryId);
206
- const connection = lib.get(key);
220
+ respond = (key: string, message: ServerMessage) => {
221
+ const connection = this.connections.get(key);
207
222
  if (connection) {
208
223
  connection.respond(message);
209
224
  } else {
210
- throw new Error(
211
- `No connection found for library ${libraryId}, key ${key}`,
212
- );
225
+ throw new Error(`No connection found for key ${key}`);
213
226
  }
214
227
  };
215
228
 
@@ -217,26 +230,20 @@ export class ClientConnectionManager {
217
230
  * Broadcasts a message to all live-connected clients in a library.
218
231
  * This does not reach push/pull clients.
219
232
  */
220
- broadcast = (
221
- libraryId: string,
222
- message: ServerMessage,
223
- omitKeys: string[] = [],
224
- ) => {
225
- const lib = this.getLibraryConnections(libraryId);
226
- for (const [key, connection] of lib) {
233
+ broadcast = (message: ServerMessage, omitKeys: string[] = []) => {
234
+ for (const [key, connection] of this.connections) {
227
235
  if (!omitKeys.includes(key)) {
228
236
  connection.respond(message);
229
237
  }
230
238
  }
231
239
  };
232
240
 
233
- disconnectAll = (libraryId: string) => {
234
- const lib = this.getLibraryConnections(libraryId);
235
- for (const connection of lib.values()) {
241
+ disconnectAll = () => {
242
+ for (const connection of this.connections.values()) {
236
243
  if (connection instanceof WebSocketClientConnection) {
237
244
  connection.ws.close();
238
245
  }
239
246
  }
240
- lib.clear();
247
+ this.connections.clear();
241
248
  };
242
249
  }
@@ -0,0 +1,6 @@
1
+ import { ServerMessage } from '@verdant-web/common';
2
+
3
+ export interface MessageSender {
4
+ broadcast(message: ServerMessage, omitKeys?: string[]): void;
5
+ respond(clientKey: string, message: ServerMessage): void;
6
+ }
@@ -0,0 +1,95 @@
1
+ import {
2
+ EventSubscriber,
3
+ initialInternalPresence,
4
+ UserInfo,
5
+ UserInfoUpdate,
6
+ } from '@verdant-web/common';
7
+ import { UserProfiles } from '../Profiles.js';
8
+
9
+ export type PresenceEvents = {
10
+ lost: (connectionKey: string, userId: string) => void;
11
+ };
12
+
13
+ /**
14
+ * Stores client presence in-memory for connected
15
+ * clients
16
+ */
17
+ export class Presence extends EventSubscriber<PresenceEvents> {
18
+ private presences: Record<string, UserInfo<any, any>> = {};
19
+ private connectionToUser: Record<string, string> = {};
20
+ private userToConnection: Record<string, Set<string>> = {};
21
+ private keepalives = new Map<string, NodeJS.Timeout>();
22
+
23
+ constructor(private profiles: UserProfiles<any>) {
24
+ super();
25
+ }
26
+
27
+ set = async (
28
+ connectionKey: string,
29
+ userId: string,
30
+ userInfo: UserInfoUpdate,
31
+ ) => {
32
+ if (!this.presences[userId]) {
33
+ this.presences[userId] = {
34
+ ...userInfo,
35
+ profile: await this.profiles.get(userId),
36
+ internal: userInfo.internal || initialInternalPresence,
37
+ };
38
+ } else {
39
+ Object.assign(this.presences[userId], userInfo);
40
+ }
41
+ this.connectionToUser[connectionKey] = userId;
42
+ this.userToConnection[userId] =
43
+ this.userToConnection[userId] || new Set<string>();
44
+ this.userToConnection[userId].add(connectionKey);
45
+
46
+ this.keepAlive(connectionKey);
47
+
48
+ return this.presences[userId]!;
49
+ };
50
+
51
+ keepAlive = (connectionKey: string) => {
52
+ const existing = this.keepalives.get(connectionKey);
53
+ if (existing) {
54
+ clearTimeout(existing);
55
+ }
56
+
57
+ this.keepalives.set(
58
+ connectionKey,
59
+ setTimeout(() => {
60
+ this.removeConnection(connectionKey);
61
+ this.keepalives.delete(connectionKey);
62
+ }, 30 * 1000),
63
+ );
64
+ };
65
+
66
+ get = (userId: string) => {
67
+ return this.presences[userId];
68
+ };
69
+
70
+ removeConnection = (connectionKey: string) => {
71
+ const userId = this.connectionToUser[connectionKey];
72
+ if (!userId) return;
73
+
74
+ this.userToConnection[userId].delete(connectionKey);
75
+ if (this.userToConnection[userId].size === 0) {
76
+ delete this.presences[userId];
77
+ this.emit('lost', connectionKey, userId);
78
+
79
+ // memory cleanup
80
+ if (Object.keys(this.presences).length === 0) {
81
+ this.clear();
82
+ }
83
+ }
84
+ };
85
+
86
+ all = () => {
87
+ return this.presences;
88
+ };
89
+
90
+ clear = () => {
91
+ this.presences = {};
92
+ this.connectionToUser = {};
93
+ this.userToConnection = {};
94
+ };
95
+ }
package/src/core.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { FileStorage } from './files/FileStorage.js';
2
+ import { SingleNodeLibraryManager } from './libraries/singleNode.js';
3
+ import { Logger } from './logger.js';
4
+ import { UserProfileLoader, UserProfiles } from './Profiles.js';
5
+ import { StorageFactory } from './storage/Storage.js';
6
+
7
+ export function createVerdant({
8
+ profiles: profilesSource,
9
+ storage,
10
+ tokenSecret,
11
+ disableRebasing,
12
+ fileStorage,
13
+ log,
14
+ __testMode,
15
+ }: {
16
+ profiles: UserProfiles<any>;
17
+ storage: StorageFactory;
18
+ tokenSecret: string;
19
+ disableRebasing?: boolean;
20
+ fileStorage?: FileStorage;
21
+ log?: Logger;
22
+ __testMode?: boolean;
23
+ }) {
24
+ const profiles = new UserProfileLoader(profilesSource);
25
+ return new SingleNodeLibraryManager({
26
+ profiles,
27
+ storage,
28
+ tokenSecret,
29
+ disableRebasing,
30
+ fileStorage,
31
+ log,
32
+ __testMode,
33
+ });
34
+ }
35
+
36
+ export type VerdantCore = ReturnType<typeof createVerdant>;
@@ -0,0 +1,2 @@
1
+ export const defaultFileExpirationDays = 14;
2
+ export const defaultReplicaTruancyMinutes = 7 * 24 * 60; // 7 days
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { Readable } from 'stream';
3
+ import type { Readable } from 'stream';
4
+ import { ReadableStream } from 'stream/web';
4
5
 
5
6
  export interface FileInfo {
6
7
  fileName: string;
@@ -18,7 +19,10 @@ export interface FileInfo {
18
19
  * even if the file is not yet uploaded, based on its metadata.
19
20
  */
20
21
  export interface FileStorage {
21
- put(fileStream: Readable, data: FileInfo): Promise<void>;
22
+ put(
23
+ fileStream: Readable | ReadableStream | any,
24
+ data: FileInfo,
25
+ ): Promise<void>;
22
26
  getUrl(data: FileInfo): string | Promise<string>;
23
27
  delete(data: FileInfo): Promise<void>;
24
28
  }
@@ -49,7 +53,7 @@ export class LocalFileStorage implements FileStorage {
49
53
  return host;
50
54
  };
51
55
 
52
- async put(fileStream: Readable, data: FileInfo): Promise<void> {
56
+ async put(fileStream: any, data: FileInfo): Promise<void> {
53
57
  this.log('debug', 'Saving file', data.id, data.fileName);
54
58
  const filePath = this.getPath(data);
55
59
  const containingDirectory = path.dirname(
@@ -61,6 +65,10 @@ export class LocalFileStorage implements FileStorage {
61
65
  const location = this.getStorageLocation(data);
62
66
  const dest = fs.createWriteStream(location);
63
67
  this.log('info', 'File saving to', location);
68
+ if (fileStream instanceof ReadableStream) {
69
+ // @ts-ignore
70
+ fileStream = Readable.fromWeb(fileStream);
71
+ }
64
72
  fileStream.pipe(dest);
65
73
  }
66
74
 
@@ -100,3 +108,28 @@ export class LocalFileStorage implements FileStorage {
100
108
  return path.join(this.rootDirectory, this.getPath(data));
101
109
  }
102
110
  }
111
+
112
+ export class FileStorageLibraryDelegate {
113
+ constructor(
114
+ private libraryId: string,
115
+ private fileStorage: FileStorage,
116
+ ) {}
117
+ put = (fileStream: Readable, data: Omit<FileInfo, 'libraryId'>) => {
118
+ return this.fileStorage.put(fileStream, {
119
+ ...data,
120
+ libraryId: this.libraryId,
121
+ });
122
+ };
123
+ getUrl = (data: Omit<FileInfo, 'libraryId'>) => {
124
+ return this.fileStorage.getUrl({
125
+ ...data,
126
+ libraryId: this.libraryId,
127
+ });
128
+ };
129
+ delete = (data: Omit<FileInfo, 'libraryId'>) => {
130
+ return this.fileStorage.delete({
131
+ ...data,
132
+ libraryId: this.libraryId,
133
+ });
134
+ };
135
+ }
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
- export type { MessageSender } from './MessageSender.js';
2
- export type { UserProfiles } from './Profiles.js';
3
- export type { ClientMessage, ServerMessage } from '@verdant-web/common';
4
1
  export { ReplicaType } from '@verdant-web/common';
5
- export { Server } from './Server.js';
6
- export { TokenProvider } from './TokenProvider.js';
2
+ export type { ClientMessage, ServerMessage } from '@verdant-web/common';
3
+ export { createVerdant, type VerdantCore } from './core.js';
7
4
  export { LocalFileStorage } from './files/FileStorage.js';
8
- export type { FileStorage, FileInfo } from './files/FileStorage.js';
9
- export type { TokenInfo } from './TokenVerifier.js';
5
+ export type { FileInfo, FileStorage } from './files/FileStorage.js';
6
+ export { type LibraryApi } from './libraries/generic.js';
7
+ export type { UserProfiles } from './Profiles.js';
8
+ export { TokenProvider } from './TokenProvider.js';
9
+ export { type TokenInfo } from './TokenVerifier.js';
10
10
  export type { LibraryInfo } from './types.js';