@evolu/nodejs 1.0.1-preview.9 → 2.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.
@@ -1 +1 @@
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
+ {"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../src/Evolu/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EAIV,qBAAqB,EAKrB,MAAM,EAEN,WAAW,EAGZ,MAAM,eAAe,CAAC;AACvB,OAAO,EASL,KAAK,EACL,WAAW,EACZ,MAAM,qBAAqB,CAAC;AAO7B,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,5 +1,6 @@
1
- import { createManyToManyMap, createRandom, createSqlite, ok, SimpleName, Uint8Array, } from "@evolu/common";
2
- import { applyProtocolMessageAsRelay, createRelayLogger, createRelaySqliteStorage, defaultProtocolMessageMaxSize, parseOwnerIdFromUrl, } from "@evolu/common/evolu";
1
+ import { createRelation, createRandom, createSqlite, isAsync, ok, SimpleName, Uint8Array, } from "@evolu/common";
2
+ import { applyProtocolMessageAsRelay, createBaseSqliteStorageTables, createRelayLogger, createRelaySqliteStorage, createRelayStorageTables, defaultProtocolMessageMaxSize, parseOwnerIdFromOwnerWebSocketTransportUrl, } from "@evolu/common/evolu";
3
+ import { existsSync } from "fs";
3
4
  import { createServer } from "http";
4
5
  import { WebSocket, WebSocketServer } from "ws";
5
6
  import { createBetterSqliteDriver } from "../BetterSqliteDriver.js";
@@ -26,25 +27,32 @@ export const createNodeJsRelayWithSqliteDriver = (deps) => (config) => createNod
26
27
  random: createRandom(),
27
28
  timingSafeEqual: createTimingSafeEqual(),
28
29
  })(config);
29
- const createNodeJsRelayWithDeps = (deps) => async ({ port = 443, name = SimpleName.orThrow("evolu-relay"), enableLogging = false, authenticateOwner, }) => {
30
+ const createNodeJsRelayWithDeps = (deps) => async ({ port = 443, name = SimpleName.orThrow("evolu-relay"), enableLogging = false, isOwnerAllowed, isOwnerWithinQuota, }) => {
30
31
  const log = createRelayLogger(deps);
31
32
  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 })({
33
+ const dbFileExists = existsSync(`${name}.db`);
34
+ const sqlite = await createSqlite(deps)(name);
35
+ if (!sqlite.ok)
36
+ return sqlite;
37
+ const depsWithSqlite = { ...deps, sqlite: sqlite.value };
38
+ if (!dbFileExists) {
39
+ const baseTables = createBaseSqliteStorageTables(depsWithSqlite);
40
+ if (!baseTables.ok)
41
+ return baseTables;
42
+ const relayTables = createRelayStorageTables(depsWithSqlite);
43
+ if (!relayTables.ok)
44
+ return relayTables;
45
+ }
46
+ const storage = createRelaySqliteStorage(depsWithSqlite)({
37
47
  onStorageError: log.storageError,
48
+ isOwnerWithinQuota,
38
49
  });
39
- if (!storageResult.ok)
40
- return storageResult;
41
- const storage = storageResult.value;
42
50
  const server = createServer();
43
51
  const wss = new WebSocketServer({
44
52
  maxPayload: defaultProtocolMessageMaxSize,
45
53
  noServer: true,
46
54
  });
47
- const ownerSocketsMap = createManyToManyMap();
55
+ const ownerSocketRelation = createRelation();
48
56
  server.on("upgrade", (request, socket, head) => {
49
57
  socket.on("error", log.upgradeSocketError);
50
58
  const completeUpgrade = () => {
@@ -53,32 +61,28 @@ const createNodeJsRelayWithDeps = (deps) => async ({ port = 443, name = SimpleNa
53
61
  wss.emit("connection", ws, request);
54
62
  });
55
63
  };
56
- if (!authenticateOwner) {
64
+ if (!isOwnerAllowed) {
57
65
  completeUpgrade();
58
66
  return;
59
67
  }
60
- const ownerId = parseOwnerIdFromUrl(request.url);
68
+ const ownerId = parseOwnerIdFromOwnerWebSocketTransportUrl(request.url ?? "");
61
69
  if (!ownerId) {
62
70
  log.invalidOrMissingOwnerIdInUrl(request.url);
63
71
  socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
64
72
  socket.destroy();
65
73
  return;
66
74
  }
67
- authenticateOwner(ownerId)
68
- .then((isAuthenticated) => {
69
- if (!isAuthenticated) {
75
+ void (async () => {
76
+ const result = isOwnerAllowed(ownerId);
77
+ const isAllowed = isAsync(result) ? await result : result;
78
+ if (!isAllowed) {
70
79
  log.unauthorizedOwner(ownerId);
71
80
  socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
72
81
  socket.destroy();
73
82
  return;
74
83
  }
75
84
  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
- });
85
+ })();
82
86
  });
83
87
  wss.on("connection", (ws) => {
84
88
  log.connectionEstablished(wss.clients.size);
@@ -87,15 +91,15 @@ const createNodeJsRelayWithDeps = (deps) => async ({ port = 443, name = SimpleNa
87
91
  });
88
92
  const options = {
89
93
  subscribe: (ownerId) => {
90
- ownerSocketsMap.add(ownerId, ws);
91
- log.relayOptionSubscribe(ownerId, () => ownerSocketsMap.getValues(ownerId)?.size ?? 0);
94
+ ownerSocketRelation.add(ownerId, ws);
95
+ log.relayOptionSubscribe(ownerId, () => ownerSocketRelation.getB(ownerId)?.size ?? 0);
92
96
  },
93
97
  unsubscribe: (ownerId) => {
94
- ownerSocketsMap.remove(ownerId, ws);
95
- log.relayOptionUnsubscribe(ownerId, () => ownerSocketsMap.getValues(ownerId)?.size ?? 0);
98
+ ownerSocketRelation.remove(ownerId, ws);
99
+ log.relayOptionUnsubscribe(ownerId, () => ownerSocketRelation.getB(ownerId)?.size ?? 0);
96
100
  },
97
101
  broadcast: (ownerId, message) => {
98
- const sockets = ownerSocketsMap.getValues(ownerId);
102
+ const sockets = ownerSocketRelation.getB(ownerId);
99
103
  if (!sockets)
100
104
  return;
101
105
  let broadcastCount = 0;
@@ -124,7 +128,7 @@ const createNodeJsRelayWithDeps = (deps) => async ({ port = 443, name = SimpleNa
124
128
  .catch(log.applyProtocolMessageAsRelayUnknownError);
125
129
  });
126
130
  ws.on("close", () => {
127
- ownerSocketsMap.deleteValue(ws);
131
+ ownerSocketRelation.deleteB(ws);
128
132
  log.connectionClosed(wss.clients.size);
129
133
  });
130
134
  });
@@ -149,7 +153,7 @@ const createNodeJsRelayWithDeps = (deps) => async ({ port = 443, name = SimpleNa
149
153
  if (isDisposed)
150
154
  return;
151
155
  isDisposed = true;
152
- sqlite[Symbol.dispose]();
156
+ sqlite.value[Symbol.dispose]();
153
157
  dispose();
154
158
  },
155
159
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/nodejs",
3
- "version": "1.0.1-preview.9",
3
+ "version": "2.0.0",
4
4
  "description": "Evolu for Node.js",
5
5
  "author": "Daniel Steigerwald <daniel@steigerwald.cz>",
6
6
  "license": "MIT",
@@ -30,11 +30,11 @@
30
30
  "shx": "^0.4.0",
31
31
  "typescript": "^5.9.2",
32
32
  "vitest": "^4.0.4",
33
- "@evolu/common": "6.0.1-preview.21",
33
+ "@evolu/common": "7.0.0",
34
34
  "@evolu/tsconfig": "0.0.2"
35
35
  },
36
36
  "peerDependencies": {
37
- "@evolu/common": "^6.0.1-preview.21"
37
+ "@evolu/common": "^7.0.0"
38
38
  },
39
39
  "engines": {
40
40
  "node": ">=22.0.0"
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  ConsoleDep,
3
- createManyToManyMap,
3
+ createRelation,
4
4
  createRandom,
5
5
  createSqlite,
6
6
  CreateSqliteDriverDep,
7
+ isAsync,
7
8
  ok,
8
9
  OwnerId,
9
10
  RandomDep,
@@ -16,13 +17,16 @@ import {
16
17
  import {
17
18
  applyProtocolMessageAsRelay,
18
19
  ApplyProtocolMessageAsRelayOptions,
20
+ createBaseSqliteStorageTables,
19
21
  createRelayLogger,
20
22
  createRelaySqliteStorage,
23
+ createRelayStorageTables,
21
24
  defaultProtocolMessageMaxSize,
22
- parseOwnerIdFromUrl,
25
+ parseOwnerIdFromOwnerWebSocketTransportUrl,
23
26
  Relay,
24
27
  RelayConfig,
25
28
  } from "@evolu/common/evolu";
29
+ import { existsSync } from "fs";
26
30
  import { createServer } from "http";
27
31
  import { WebSocket, WebSocketServer } from "ws";
28
32
  import { createBetterSqliteDriver } from "../BetterSqliteDriver.js";
@@ -69,29 +73,39 @@ const createNodeJsRelayWithDeps =
69
73
  port = 443,
70
74
  name = SimpleName.orThrow("evolu-relay"),
71
75
  enableLogging = false,
72
- authenticateOwner,
76
+ isOwnerAllowed,
77
+ isOwnerWithinQuota,
73
78
  }: NodeJsRelayConfig): Promise<Result<Relay, SqliteError>> => {
74
79
  const log = createRelayLogger(deps);
75
80
  log.started(enableLogging, port);
76
81
 
77
- const sqliteResult = await createSqlite(deps)(name);
78
- if (!sqliteResult.ok) return sqliteResult;
79
- const sqlite = sqliteResult.value;
82
+ const dbFileExists = existsSync(`${name}.db`);
80
83
 
81
- const storageResult = createRelaySqliteStorage({ ...deps, sqlite })({
84
+ const sqlite = await createSqlite(deps)(name);
85
+ if (!sqlite.ok) return sqlite;
86
+
87
+ const depsWithSqlite = { ...deps, sqlite: sqlite.value };
88
+
89
+ if (!dbFileExists) {
90
+ const baseTables = createBaseSqliteStorageTables(depsWithSqlite);
91
+ if (!baseTables.ok) return baseTables;
92
+
93
+ const relayTables = createRelayStorageTables(depsWithSqlite);
94
+ if (!relayTables.ok) return relayTables;
95
+ }
96
+
97
+ const storage = createRelaySqliteStorage(depsWithSqlite)({
82
98
  onStorageError: log.storageError,
99
+ isOwnerWithinQuota,
83
100
  });
84
101
 
85
- if (!storageResult.ok) return storageResult;
86
- const storage = storageResult.value;
87
-
88
102
  const server = createServer();
89
103
  const wss = new WebSocketServer({
90
104
  maxPayload: defaultProtocolMessageMaxSize,
91
105
  noServer: true,
92
106
  });
93
107
 
94
- const ownerSocketsMap = createManyToManyMap<OwnerId, WebSocket>();
108
+ const ownerSocketRelation = createRelation<OwnerId, WebSocket>();
95
109
 
96
110
  server.on("upgrade", (request, socket, head) => {
97
111
  socket.on("error", log.upgradeSocketError);
@@ -103,12 +117,15 @@ const createNodeJsRelayWithDeps =
103
117
  });
104
118
  };
105
119
 
106
- if (!authenticateOwner) {
120
+ if (!isOwnerAllowed) {
107
121
  completeUpgrade();
108
122
  return;
109
123
  }
110
124
 
111
- const ownerId = parseOwnerIdFromUrl(request.url);
125
+ const ownerId = parseOwnerIdFromOwnerWebSocketTransportUrl(
126
+ request.url ?? "",
127
+ );
128
+
112
129
  if (!ownerId) {
113
130
  log.invalidOrMissingOwnerIdInUrl(request.url);
114
131
  socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
@@ -116,21 +133,17 @@ const createNodeJsRelayWithDeps =
116
133
  return;
117
134
  }
118
135
 
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");
136
+ void (async () => {
137
+ const result = isOwnerAllowed(ownerId);
138
+ const isAllowed = isAsync(result) ? await result : result;
139
+ if (!isAllowed) {
140
+ log.unauthorizedOwner(ownerId);
141
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
132
142
  socket.destroy();
133
- });
143
+ return;
144
+ }
145
+ completeUpgrade();
146
+ })();
134
147
  });
135
148
 
136
149
  wss.on("connection", (ws) => {
@@ -142,23 +155,23 @@ const createNodeJsRelayWithDeps =
142
155
 
143
156
  const options: ApplyProtocolMessageAsRelayOptions = {
144
157
  subscribe: (ownerId) => {
145
- ownerSocketsMap.add(ownerId, ws);
158
+ ownerSocketRelation.add(ownerId, ws);
146
159
  log.relayOptionSubscribe(
147
160
  ownerId,
148
- () => ownerSocketsMap.getValues(ownerId)?.size ?? 0,
161
+ () => ownerSocketRelation.getB(ownerId)?.size ?? 0,
149
162
  );
150
163
  },
151
164
 
152
165
  unsubscribe: (ownerId) => {
153
- ownerSocketsMap.remove(ownerId, ws);
166
+ ownerSocketRelation.remove(ownerId, ws);
154
167
  log.relayOptionUnsubscribe(
155
168
  ownerId,
156
- () => ownerSocketsMap.getValues(ownerId)?.size ?? 0,
169
+ () => ownerSocketRelation.getB(ownerId)?.size ?? 0,
157
170
  );
158
171
  },
159
172
 
160
173
  broadcast: (ownerId, message) => {
161
- const sockets = ownerSocketsMap.getValues(ownerId);
174
+ const sockets = ownerSocketRelation.getB(ownerId);
162
175
  if (!sockets) return;
163
176
 
164
177
  let broadcastCount = 0;
@@ -190,7 +203,7 @@ const createNodeJsRelayWithDeps =
190
203
  });
191
204
 
192
205
  ws.on("close", () => {
193
- ownerSocketsMap.deleteValue(ws);
206
+ ownerSocketRelation.deleteB(ws);
194
207
  log.connectionClosed(wss.clients.size);
195
208
  });
196
209
  });
@@ -221,7 +234,7 @@ const createNodeJsRelayWithDeps =
221
234
  [Symbol.dispose]: () => {
222
235
  if (isDisposed) return;
223
236
  isDisposed = true;
224
- sqlite[Symbol.dispose]();
237
+ sqlite.value[Symbol.dispose]();
225
238
  dispose();
226
239
  },
227
240
  };