@evolu/nodejs 2.3.0 → 3.0.0-next.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,165 +0,0 @@
1
- import { createRelation, createRandom, createSqlite, isAsync, ok, SimpleName, Uint8Array, } from "@evolu/common";
2
- import { applyProtocolMessageAsRelay, createBaseSqliteStorageTables, createRelayLogger, createRelaySqliteStorage, createRelayStorageTables, defaultProtocolMessageMaxSize, parseOwnerIdFromOwnerWebSocketTransportUrl, } from "@evolu/common/local-first";
3
- import { existsSync } from "fs";
4
- import { createServer } from "http";
5
- import { WebSocket, WebSocketServer } from "ws";
6
- import { createBetterSqliteDriver } from "../BetterSqliteDriver.js";
7
- import { createTimingSafeEqual } from "../Crypto.js";
8
- /**
9
- * Creates an Evolu relay server.
10
- *
11
- * This implementation uses Node.js and better-sqlite3. Additional relay
12
- * implementations will be provided for other platforms (Bun, Deno, Cloudflare
13
- * Workers, Vercel Edge, etc.).
14
- */
15
- export const createNodeJsRelay = (deps) => (config) => createNodeJsRelayWithSqliteDriver({
16
- ...deps,
17
- createSqliteDriver: createBetterSqliteDriver,
18
- })(config);
19
- /**
20
- * Creates an Evolu relay server with a custom SQLite driver.
21
- *
22
- * Use this when you need to provide a different SQLite driver implementation
23
- * (e.g., using alternative SQLite libraries).
24
- */
25
- export const createNodeJsRelayWithSqliteDriver = (deps) => (config) => createNodeJsRelayWithDeps({
26
- ...deps,
27
- random: createRandom(),
28
- timingSafeEqual: createTimingSafeEqual(),
29
- })(config);
30
- const createNodeJsRelayWithDeps = (deps) => async ({ port = 443, name = SimpleName.orThrow("evolu-relay"), enableLogging = false, isOwnerAllowed, isOwnerWithinQuota, }) => {
31
- const log = createRelayLogger(deps);
32
- log.started(enableLogging, port);
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
- {
40
- const result = createBaseSqliteStorageTables(depsWithSqlite);
41
- if (!result.ok)
42
- return result;
43
- }
44
- {
45
- const result = createRelayStorageTables(depsWithSqlite);
46
- if (!result.ok)
47
- return result;
48
- }
49
- }
50
- const storage = createRelaySqliteStorage(depsWithSqlite)({
51
- onStorageError: log.storageError,
52
- isOwnerWithinQuota,
53
- });
54
- const server = createServer();
55
- const wss = new WebSocketServer({
56
- maxPayload: defaultProtocolMessageMaxSize,
57
- noServer: true,
58
- });
59
- const ownerSocketRelation = createRelation();
60
- server.on("upgrade", (request, socket, head) => {
61
- socket.on("error", log.upgradeSocketError);
62
- const completeUpgrade = () => {
63
- socket.removeListener("error", log.upgradeSocketError);
64
- wss.handleUpgrade(request, socket, head, (ws) => {
65
- wss.emit("connection", ws, request);
66
- });
67
- };
68
- if (!isOwnerAllowed) {
69
- completeUpgrade();
70
- return;
71
- }
72
- const ownerId = parseOwnerIdFromOwnerWebSocketTransportUrl(request.url ?? "");
73
- if (!ownerId) {
74
- log.invalidOrMissingOwnerIdInUrl(request.url);
75
- socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
76
- socket.destroy();
77
- return;
78
- }
79
- void (async () => {
80
- const result = isOwnerAllowed(ownerId);
81
- const isAllowed = isAsync(result) ? await result : result;
82
- if (!isAllowed) {
83
- log.unauthorizedOwner(ownerId);
84
- socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
85
- socket.destroy();
86
- return;
87
- }
88
- completeUpgrade();
89
- })();
90
- });
91
- wss.on("connection", (ws) => {
92
- log.connectionEstablished(wss.clients.size);
93
- ws.on("error", (error) => {
94
- log.connectionWebSocketError(error);
95
- });
96
- const options = {
97
- subscribe: (ownerId) => {
98
- ownerSocketRelation.add(ownerId, ws);
99
- log.relayOptionSubscribe(ownerId, () => ownerSocketRelation.getB(ownerId)?.size ?? 0);
100
- },
101
- unsubscribe: (ownerId) => {
102
- ownerSocketRelation.remove(ownerId, ws);
103
- log.relayOptionUnsubscribe(ownerId, () => ownerSocketRelation.getB(ownerId)?.size ?? 0);
104
- },
105
- broadcast: (ownerId, message) => {
106
- const sockets = ownerSocketRelation.getB(ownerId);
107
- if (!sockets)
108
- return;
109
- let broadcastCount = 0;
110
- for (const socket of sockets) {
111
- if (socket !== ws && socket.readyState === WebSocket.OPEN) {
112
- socket.send(message, { binary: true });
113
- broadcastCount++;
114
- }
115
- }
116
- log.relayOptionBroadcast(ownerId, broadcastCount, sockets.size);
117
- },
118
- };
119
- ws.on("message", (message) => {
120
- if (!Uint8Array.is(message))
121
- return;
122
- log.messageLength(message.length);
123
- applyProtocolMessageAsRelay({ storage })(message, options)
124
- .then((response) => {
125
- if (!response.ok) {
126
- log.applyProtocolMessageAsRelayError(response.error);
127
- return;
128
- }
129
- ws.send(response.value.message, { binary: true });
130
- log.responseLength(response.value.message.length);
131
- })
132
- .catch(log.applyProtocolMessageAsRelayUnknownError);
133
- });
134
- ws.on("close", () => {
135
- ownerSocketRelation.deleteB(ws);
136
- log.connectionClosed(wss.clients.size);
137
- });
138
- });
139
- server.listen(port);
140
- const dispose = () => {
141
- log.shuttingDown();
142
- wss.clients.forEach((client) => {
143
- if (client.readyState === WebSocket.OPEN) {
144
- client.close(1000, "Evolu Relay shutting down");
145
- }
146
- });
147
- wss.close(() => {
148
- log.webSocketServerDisposed();
149
- });
150
- server.close(() => {
151
- log.httpServerDisposed();
152
- });
153
- };
154
- let isDisposed = false;
155
- const relay = {
156
- [Symbol.dispose]: () => {
157
- if (isDisposed)
158
- return;
159
- isDisposed = true;
160
- sqlite.value[Symbol.dispose]();
161
- dispose();
162
- },
163
- };
164
- return ok(relay);
165
- };
@@ -1,47 +0,0 @@
1
- import {
2
- constVoid,
3
- createPreparedStatementsCache,
4
- CreateSqliteDriver,
5
- SqliteDriver,
6
- SqliteRow,
7
- } from "@evolu/common";
8
- import BetterSQLite, { Statement } from "better-sqlite3";
9
-
10
- export const createBetterSqliteDriver: CreateSqliteDriver = (name, options) => {
11
- const filename = options?.memory ? ":memory:" : `${name}.db`;
12
- const db = new BetterSQLite(filename);
13
- let isDisposed = false;
14
-
15
- const cache = createPreparedStatementsCache<Statement>(
16
- (sql) => db.prepare(sql),
17
- // Not needed.
18
- // https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#class-statement
19
- constVoid,
20
- );
21
-
22
- const driver: SqliteDriver = {
23
- exec: (query, isMutation) => {
24
- // Always prepare is recommended for better-sqlite3
25
- const prepared = cache.get(query, true);
26
-
27
- const rows = isMutation
28
- ? []
29
- : (prepared.all(query.parameters) as Array<SqliteRow>);
30
-
31
- const changes = isMutation ? prepared.run(query.parameters).changes : 0;
32
-
33
- return { rows, changes };
34
- },
35
-
36
- export: () => db.serialize(),
37
-
38
- [Symbol.dispose]: () => {
39
- if (isDisposed) return;
40
- isDisposed = true;
41
- cache[Symbol.dispose]();
42
- db.close();
43
- },
44
- };
45
-
46
- return Promise.resolve(driver);
47
- };
File without changes
File without changes