@evolu/nodejs 1.0.1-preview.7 → 1.0.1-preview.9

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.
@@ -1,7 +1,22 @@
1
- import { ConsoleDep } from "@evolu/common";
1
+ import { ConsoleDep, CreateSqliteDriverDep, Result, SqliteError } from "@evolu/common";
2
2
  import { Relay, RelayConfig } from "@evolu/common/evolu";
3
3
  export interface NodeJsRelayConfig extends RelayConfig {
4
+ /** The port number for the HTTP server. */
4
5
  readonly port?: number;
5
6
  }
6
- export declare const createNodeJsRelay: (deps: ConsoleDep) => (config: NodeJsRelayConfig) => Promise<Relay>;
7
+ /**
8
+ * Creates an Evolu relay server.
9
+ *
10
+ * This implementation uses Node.js and better-sqlite3. Additional relay
11
+ * implementations will be provided for other platforms (Bun, Deno, Cloudflare
12
+ * Workers, Vercel Edge, etc.).
13
+ */
14
+ export declare const createNodeJsRelay: (deps: ConsoleDep) => (config: NodeJsRelayConfig) => Promise<Result<Relay, SqliteError>>;
15
+ /**
16
+ * Creates an Evolu relay server with a custom SQLite driver.
17
+ *
18
+ * Use this when you need to provide a different SQLite driver implementation
19
+ * (e.g., using alternative SQLite libraries).
20
+ */
21
+ export declare const createNodeJsRelayWithSqliteDriver: (deps: ConsoleDep & CreateSqliteDriverDep) => (config: NodeJsRelayConfig) => Promise<Result<Relay, SqliteError>>;
7
22
  //# sourceMappingURL=Relay.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../src/Evolu/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EAQX,MAAM,eAAe,CAAC;AACvB,OAAO,EAKL,KAAK,EACL,WAAW,EAEZ,MAAM,qBAAqB,CAAC;AAK7B,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,iBAAiB,GAC3B,MAAM,UAAU,MACV,QAAQ,iBAAiB,KAAG,OAAO,CAAC,KAAK,CAuG/C,CAAC"}
1
+ {"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../src/Evolu/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EAIV,qBAAqB,EAIrB,MAAM,EAEN,WAAW,EAGZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAOL,KAAK,EACL,WAAW,EACZ,MAAM,qBAAqB,CAAC;AAM7B,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC3B,MAAM,UAAU,MAChB,QAAQ,iBAAiB,KAAG,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,CAInD,CAAC;AAEf;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,GAC3C,MAAM,UAAU,GAAG,qBAAqB,MACxC,QAAQ,iBAAiB,KAAG,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,CAKnD,CAAC"}
@@ -1,82 +1,157 @@
1
- import { createManyToManyMap, createRandom, createSqlite, getOrThrow, SimpleName, Uint8Array, } from "@evolu/common";
2
- import { applyProtocolMessageAsRelay, createRelayStorage, maxProtocolMessageSize, } from "@evolu/common/evolu";
1
+ import { createManyToManyMap, createRandom, createSqlite, ok, SimpleName, Uint8Array, } from "@evolu/common";
2
+ import { applyProtocolMessageAsRelay, createRelayLogger, createRelaySqliteStorage, defaultProtocolMessageMaxSize, parseOwnerIdFromUrl, } from "@evolu/common/evolu";
3
+ import { createServer } from "http";
3
4
  import { WebSocket, WebSocketServer } from "ws";
4
5
  import { createBetterSqliteDriver } from "../BetterSqliteDriver.js";
5
6
  import { createTimingSafeEqual } from "../Crypto.js";
6
- export const createNodeJsRelay = (deps) => async (config) => {
7
- const { port = 443, name = getOrThrow(SimpleName.from("evolu-relay")), enableLogging = false, } = config;
8
- deps.console.enabled = true;
9
- deps.console.log(`Evolu Relay started on port ${port}`);
10
- deps.console.enabled = enableLogging;
11
- const sqlite = getOrThrow(await createSqlite({
12
- ...deps,
13
- createSqliteDriver: createBetterSqliteDriver,
14
- })(name));
15
- const relaySqliteStorageDeps = {
16
- sqlite,
17
- random: createRandom(),
18
- timingSafeEqual: createTimingSafeEqual(),
19
- };
20
- const storage = getOrThrow(createRelayStorage(relaySqliteStorageDeps)({
21
- onStorageError: (error) => {
22
- deps.console.error("[relay]", "[storage]", error);
23
- },
24
- }));
7
+ /**
8
+ * Creates an Evolu relay server.
9
+ *
10
+ * This implementation uses Node.js and better-sqlite3. Additional relay
11
+ * implementations will be provided for other platforms (Bun, Deno, Cloudflare
12
+ * Workers, Vercel Edge, etc.).
13
+ */
14
+ export const createNodeJsRelay = (deps) => (config) => createNodeJsRelayWithSqliteDriver({
15
+ ...deps,
16
+ createSqliteDriver: createBetterSqliteDriver,
17
+ })(config);
18
+ /**
19
+ * Creates an Evolu relay server with a custom SQLite driver.
20
+ *
21
+ * Use this when you need to provide a different SQLite driver implementation
22
+ * (e.g., using alternative SQLite libraries).
23
+ */
24
+ export const createNodeJsRelayWithSqliteDriver = (deps) => (config) => createNodeJsRelayWithDeps({
25
+ ...deps,
26
+ random: createRandom(),
27
+ timingSafeEqual: createTimingSafeEqual(),
28
+ })(config);
29
+ const createNodeJsRelayWithDeps = (deps) => async ({ port = 443, name = SimpleName.orThrow("evolu-relay"), enableLogging = false, authenticateOwner, }) => {
30
+ const log = createRelayLogger(deps);
31
+ log.started(enableLogging, port);
32
+ const sqliteResult = await createSqlite(deps)(name);
33
+ if (!sqliteResult.ok)
34
+ return sqliteResult;
35
+ const sqlite = sqliteResult.value;
36
+ const storageResult = createRelaySqliteStorage({ ...deps, sqlite })({
37
+ onStorageError: log.storageError,
38
+ });
39
+ if (!storageResult.ok)
40
+ return storageResult;
41
+ const storage = storageResult.value;
42
+ const server = createServer();
25
43
  const wss = new WebSocketServer({
26
- maxPayload: maxProtocolMessageSize,
27
- port,
44
+ maxPayload: defaultProtocolMessageMaxSize,
45
+ noServer: true,
28
46
  });
29
47
  const ownerSocketsMap = createManyToManyMap();
48
+ server.on("upgrade", (request, socket, head) => {
49
+ socket.on("error", log.upgradeSocketError);
50
+ const completeUpgrade = () => {
51
+ socket.removeListener("error", log.upgradeSocketError);
52
+ wss.handleUpgrade(request, socket, head, (ws) => {
53
+ wss.emit("connection", ws, request);
54
+ });
55
+ };
56
+ if (!authenticateOwner) {
57
+ completeUpgrade();
58
+ return;
59
+ }
60
+ const ownerId = parseOwnerIdFromUrl(request.url);
61
+ if (!ownerId) {
62
+ log.invalidOrMissingOwnerIdInUrl(request.url);
63
+ socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
64
+ socket.destroy();
65
+ return;
66
+ }
67
+ authenticateOwner(ownerId)
68
+ .then((isAuthenticated) => {
69
+ if (!isAuthenticated) {
70
+ log.unauthorizedOwner(ownerId);
71
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
72
+ socket.destroy();
73
+ return;
74
+ }
75
+ completeUpgrade();
76
+ })
77
+ .catch((error) => {
78
+ log.authenticateOwnerError(error);
79
+ socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n");
80
+ socket.destroy();
81
+ });
82
+ });
30
83
  wss.on("connection", (ws) => {
31
- ws.on("error", deps.console.error);
84
+ log.connectionEstablished(wss.clients.size);
85
+ ws.on("error", (error) => {
86
+ log.connectionWebSocketError(error);
87
+ });
32
88
  const options = {
33
89
  subscribe: (ownerId) => {
34
90
  ownerSocketsMap.add(ownerId, ws);
91
+ log.relayOptionSubscribe(ownerId, () => ownerSocketsMap.getValues(ownerId)?.size ?? 0);
92
+ },
93
+ unsubscribe: (ownerId) => {
94
+ ownerSocketsMap.remove(ownerId, ws);
95
+ log.relayOptionUnsubscribe(ownerId, () => ownerSocketsMap.getValues(ownerId)?.size ?? 0);
35
96
  },
36
97
  broadcast: (ownerId, message) => {
37
98
  const sockets = ownerSocketsMap.getValues(ownerId);
99
+ if (!sockets)
100
+ return;
101
+ let broadcastCount = 0;
38
102
  for (const socket of sockets) {
39
103
  if (socket !== ws && socket.readyState === WebSocket.OPEN) {
40
104
  socket.send(message, { binary: true });
105
+ broadcastCount++;
41
106
  }
42
107
  }
108
+ log.relayOptionBroadcast(ownerId, broadcastCount, sockets.size);
43
109
  },
44
110
  };
45
111
  ws.on("message", (message) => {
46
112
  if (!Uint8Array.is(message))
47
113
  return;
48
- const response = applyProtocolMessageAsRelay({ storage })(message, options);
49
- if (!response.ok) {
50
- deps.console.warn(response.error.type);
51
- return;
52
- }
53
- if (response.value) {
54
- ws.send(response.value, { binary: true });
55
- }
114
+ log.messageLength(message.length);
115
+ applyProtocolMessageAsRelay({ storage })(message, options)
116
+ .then((response) => {
117
+ if (!response.ok) {
118
+ log.applyProtocolMessageAsRelayError(response.error);
119
+ return;
120
+ }
121
+ ws.send(response.value.message, { binary: true });
122
+ log.responseLength(response.value.message.length);
123
+ })
124
+ .catch(log.applyProtocolMessageAsRelayUnknownError);
56
125
  });
57
126
  ws.on("close", () => {
58
127
  ownerSocketsMap.deleteValue(ws);
128
+ log.connectionClosed(wss.clients.size);
59
129
  });
60
130
  });
61
- const disposeWss = () => {
62
- deps.console.log("Shutting down Evolu Relay...");
131
+ server.listen(port);
132
+ const dispose = () => {
133
+ log.shuttingDown();
63
134
  wss.clients.forEach((client) => {
64
135
  if (client.readyState === WebSocket.OPEN) {
65
136
  client.close(1000, "Evolu Relay shutting down");
66
137
  }
67
138
  });
68
139
  wss.close(() => {
69
- deps.console.log("Evolu Relay disposed");
140
+ log.webSocketServerDisposed();
141
+ });
142
+ server.close(() => {
143
+ log.httpServerDisposed();
70
144
  });
71
145
  };
72
146
  let isDisposed = false;
73
- return {
147
+ const relay = {
74
148
  [Symbol.dispose]: () => {
75
149
  if (isDisposed)
76
150
  return;
77
151
  isDisposed = true;
78
152
  sqlite[Symbol.dispose]();
79
- disposeWss();
153
+ dispose();
80
154
  },
81
155
  };
156
+ return ok(relay);
82
157
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/nodejs",
3
- "version": "1.0.1-preview.7",
3
+ "version": "1.0.1-preview.9",
4
4
  "description": "Evolu for Node.js",
5
5
  "author": "Daniel Steigerwald <daniel@steigerwald.cz>",
6
6
  "license": "MIT",
@@ -26,13 +26,15 @@
26
26
  "devDependencies": {
27
27
  "@types/better-sqlite3": "^7.6.13",
28
28
  "@types/node": "^22.17.1",
29
+ "@types/ws": "^8.18.1",
30
+ "shx": "^0.4.0",
29
31
  "typescript": "^5.9.2",
30
- "vitest": "^3.2.3",
31
- "@evolu/common": "6.0.1-preview.18",
32
+ "vitest": "^4.0.4",
33
+ "@evolu/common": "6.0.1-preview.21",
32
34
  "@evolu/tsconfig": "0.0.2"
33
35
  },
34
36
  "peerDependencies": {
35
- "@evolu/common": "^6.0.1-preview.18"
37
+ "@evolu/common": "^6.0.1-preview.21"
36
38
  },
37
39
  "engines": {
38
40
  "node": ">=22.0.0"
@@ -3,111 +3,202 @@ import {
3
3
  createManyToManyMap,
4
4
  createRandom,
5
5
  createSqlite,
6
- getOrThrow,
6
+ CreateSqliteDriverDep,
7
+ ok,
7
8
  OwnerId,
9
+ RandomDep,
10
+ Result,
8
11
  SimpleName,
12
+ SqliteError,
13
+ TimingSafeEqualDep,
9
14
  Uint8Array,
10
15
  } from "@evolu/common";
11
16
  import {
12
17
  applyProtocolMessageAsRelay,
13
18
  ApplyProtocolMessageAsRelayOptions,
14
- createRelayStorage,
15
- maxProtocolMessageSize,
19
+ createRelayLogger,
20
+ createRelaySqliteStorage,
21
+ defaultProtocolMessageMaxSize,
22
+ parseOwnerIdFromUrl,
16
23
  Relay,
17
24
  RelayConfig,
18
- RelaySqliteStorageDeps,
19
25
  } from "@evolu/common/evolu";
26
+ import { createServer } from "http";
20
27
  import { WebSocket, WebSocketServer } from "ws";
21
28
  import { createBetterSqliteDriver } from "../BetterSqliteDriver.js";
22
29
  import { createTimingSafeEqual } from "../Crypto.js";
23
30
 
24
31
  export interface NodeJsRelayConfig extends RelayConfig {
32
+ /** The port number for the HTTP server. */
25
33
  readonly port?: number;
26
34
  }
27
35
 
36
+ /**
37
+ * Creates an Evolu relay server.
38
+ *
39
+ * This implementation uses Node.js and better-sqlite3. Additional relay
40
+ * implementations will be provided for other platforms (Bun, Deno, Cloudflare
41
+ * Workers, Vercel Edge, etc.).
42
+ */
28
43
  export const createNodeJsRelay =
29
44
  (deps: ConsoleDep) =>
30
- async (config: NodeJsRelayConfig): Promise<Relay> => {
31
- const {
32
- port = 443,
33
- name = getOrThrow(SimpleName.from("evolu-relay")),
34
- enableLogging = false,
35
- } = config;
36
-
37
- deps.console.enabled = true;
38
- deps.console.log(`Evolu Relay started on port ${port}`);
39
- deps.console.enabled = enableLogging;
40
-
41
- const sqlite = getOrThrow(
42
- await createSqlite({
43
- ...deps,
44
- createSqliteDriver: createBetterSqliteDriver,
45
- })(name),
46
- );
47
-
48
- const relaySqliteStorageDeps: RelaySqliteStorageDeps = {
49
- sqlite,
45
+ (config: NodeJsRelayConfig): Promise<Result<Relay, SqliteError>> =>
46
+ createNodeJsRelayWithSqliteDriver({
47
+ ...deps,
48
+ createSqliteDriver: createBetterSqliteDriver,
49
+ })(config);
50
+
51
+ /**
52
+ * Creates an Evolu relay server with a custom SQLite driver.
53
+ *
54
+ * Use this when you need to provide a different SQLite driver implementation
55
+ * (e.g., using alternative SQLite libraries).
56
+ */
57
+ export const createNodeJsRelayWithSqliteDriver =
58
+ (deps: ConsoleDep & CreateSqliteDriverDep) =>
59
+ (config: NodeJsRelayConfig): Promise<Result<Relay, SqliteError>> =>
60
+ createNodeJsRelayWithDeps({
61
+ ...deps,
50
62
  random: createRandom(),
51
63
  timingSafeEqual: createTimingSafeEqual(),
52
- };
64
+ })(config);
65
+
66
+ const createNodeJsRelayWithDeps =
67
+ (deps: ConsoleDep & CreateSqliteDriverDep & RandomDep & TimingSafeEqualDep) =>
68
+ async ({
69
+ port = 443,
70
+ name = SimpleName.orThrow("evolu-relay"),
71
+ enableLogging = false,
72
+ authenticateOwner,
73
+ }: NodeJsRelayConfig): Promise<Result<Relay, SqliteError>> => {
74
+ const log = createRelayLogger(deps);
75
+ log.started(enableLogging, port);
76
+
77
+ const sqliteResult = await createSqlite(deps)(name);
78
+ if (!sqliteResult.ok) return sqliteResult;
79
+ const sqlite = sqliteResult.value;
80
+
81
+ const storageResult = createRelaySqliteStorage({ ...deps, sqlite })({
82
+ onStorageError: log.storageError,
83
+ });
53
84
 
54
- const storage = getOrThrow(
55
- createRelayStorage(relaySqliteStorageDeps)({
56
- onStorageError: (error) => {
57
- deps.console.error("[relay]", "[storage]", error);
58
- },
59
- }),
60
- );
85
+ if (!storageResult.ok) return storageResult;
86
+ const storage = storageResult.value;
61
87
 
88
+ const server = createServer();
62
89
  const wss = new WebSocketServer({
63
- maxPayload: maxProtocolMessageSize,
64
- port,
90
+ maxPayload: defaultProtocolMessageMaxSize,
91
+ noServer: true,
65
92
  });
66
93
 
67
94
  const ownerSocketsMap = createManyToManyMap<OwnerId, WebSocket>();
68
95
 
96
+ server.on("upgrade", (request, socket, head) => {
97
+ socket.on("error", log.upgradeSocketError);
98
+
99
+ const completeUpgrade = () => {
100
+ socket.removeListener("error", log.upgradeSocketError);
101
+ wss.handleUpgrade(request, socket, head, (ws) => {
102
+ wss.emit("connection", ws, request);
103
+ });
104
+ };
105
+
106
+ if (!authenticateOwner) {
107
+ completeUpgrade();
108
+ return;
109
+ }
110
+
111
+ const ownerId = parseOwnerIdFromUrl(request.url);
112
+ if (!ownerId) {
113
+ log.invalidOrMissingOwnerIdInUrl(request.url);
114
+ socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
115
+ socket.destroy();
116
+ return;
117
+ }
118
+
119
+ authenticateOwner(ownerId)
120
+ .then((isAuthenticated) => {
121
+ if (!isAuthenticated) {
122
+ log.unauthorizedOwner(ownerId);
123
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
124
+ socket.destroy();
125
+ return;
126
+ }
127
+ completeUpgrade();
128
+ })
129
+ .catch((error: unknown) => {
130
+ log.authenticateOwnerError(error);
131
+ socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n");
132
+ socket.destroy();
133
+ });
134
+ });
135
+
69
136
  wss.on("connection", (ws) => {
70
- ws.on("error", deps.console.error);
137
+ log.connectionEstablished(wss.clients.size);
138
+
139
+ ws.on("error", (error) => {
140
+ log.connectionWebSocketError(error);
141
+ });
71
142
 
72
143
  const options: ApplyProtocolMessageAsRelayOptions = {
73
144
  subscribe: (ownerId) => {
74
145
  ownerSocketsMap.add(ownerId, ws);
146
+ log.relayOptionSubscribe(
147
+ ownerId,
148
+ () => ownerSocketsMap.getValues(ownerId)?.size ?? 0,
149
+ );
150
+ },
151
+
152
+ unsubscribe: (ownerId) => {
153
+ ownerSocketsMap.remove(ownerId, ws);
154
+ log.relayOptionUnsubscribe(
155
+ ownerId,
156
+ () => ownerSocketsMap.getValues(ownerId)?.size ?? 0,
157
+ );
75
158
  },
159
+
76
160
  broadcast: (ownerId, message) => {
77
- const sockets = ownerSocketsMap.getValues(ownerId)!;
161
+ const sockets = ownerSocketsMap.getValues(ownerId);
162
+ if (!sockets) return;
163
+
164
+ let broadcastCount = 0;
78
165
  for (const socket of sockets) {
79
166
  if (socket !== ws && socket.readyState === WebSocket.OPEN) {
80
167
  socket.send(message, { binary: true });
168
+ broadcastCount++;
81
169
  }
82
170
  }
171
+
172
+ log.relayOptionBroadcast(ownerId, broadcastCount, sockets.size);
83
173
  },
84
174
  };
85
175
 
86
176
  ws.on("message", (message) => {
87
177
  if (!Uint8Array.is(message)) return;
178
+ log.messageLength(message.length);
88
179
 
89
- const response = applyProtocolMessageAsRelay({ storage })(
90
- message,
91
- options,
92
- );
93
-
94
- if (!response.ok) {
95
- deps.console.warn(response.error.type);
96
- return;
97
- }
98
-
99
- if (response.value) {
100
- ws.send(response.value, { binary: true });
101
- }
180
+ applyProtocolMessageAsRelay({ storage })(message, options)
181
+ .then((response) => {
182
+ if (!response.ok) {
183
+ log.applyProtocolMessageAsRelayError(response.error);
184
+ return;
185
+ }
186
+ ws.send(response.value.message, { binary: true });
187
+ log.responseLength(response.value.message.length);
188
+ })
189
+ .catch(log.applyProtocolMessageAsRelayUnknownError);
102
190
  });
103
191
 
104
192
  ws.on("close", () => {
105
193
  ownerSocketsMap.deleteValue(ws);
194
+ log.connectionClosed(wss.clients.size);
106
195
  });
107
196
  });
108
197
 
109
- const disposeWss = () => {
110
- deps.console.log("Shutting down Evolu Relay...");
198
+ server.listen(port);
199
+
200
+ const dispose = () => {
201
+ log.shuttingDown();
111
202
 
112
203
  wss.clients.forEach((client) => {
113
204
  if (client.readyState === WebSocket.OPEN) {
@@ -116,18 +207,24 @@ export const createNodeJsRelay =
116
207
  });
117
208
 
118
209
  wss.close(() => {
119
- deps.console.log("Evolu Relay disposed");
210
+ log.webSocketServerDisposed();
211
+ });
212
+
213
+ server.close(() => {
214
+ log.httpServerDisposed();
120
215
  });
121
216
  };
122
217
 
123
218
  let isDisposed = false;
124
219
 
125
- return {
220
+ const relay: Relay = {
126
221
  [Symbol.dispose]: () => {
127
222
  if (isDisposed) return;
128
223
  isDisposed = true;
129
224
  sqlite[Symbol.dispose]();
130
- disposeWss();
225
+ dispose();
131
226
  },
132
227
  };
228
+
229
+ return ok(relay);
133
230
  };