@evolu/nodejs 3.0.0-next.2 → 3.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/dist/src/Cli.d.ts +29 -0
- package/dist/src/Cli.d.ts.map +1 -0
- package/dist/src/Cli.js +49 -0
- package/dist/src/Platform.d.ts +15 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +9 -0
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +125 -35
- package/dist/src/Task.d.ts +69 -24
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +148 -52
- package/dist/src/TestBundle.d.ts +113 -0
- package/dist/src/TestBundle.d.ts.map +1 -0
- package/dist/src/TestBundle.js +503 -0
- package/dist/src/Time.d.ts +28 -0
- package/dist/src/Time.d.ts.map +1 -0
- package/dist/src/Time.js +25 -0
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/Worker.d.ts +3 -1
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +93 -1
- package/dist/src/index.d.ts +13 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -1
- package/dist/src/local-first/Relay.d.ts +13 -20
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +42 -48
- package/package.json +30 -6
- package/src/Cli.ts +78 -0
- package/src/Platform.ts +20 -0
- package/src/Sqlite.ts +30 -11
- package/src/Task.ts +172 -63
- package/src/TestBundle.ts +674 -0
- package/src/Time.ts +55 -0
- package/src/Worker.ts +44 -0
- package/src/index.ts +14 -5
- package/src/local-first/Relay.ts +54 -58
package/src/Time.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js-specific time utilities.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
assert,
|
|
9
|
+
createTime,
|
|
10
|
+
type Brand,
|
|
11
|
+
Millis,
|
|
12
|
+
type Time,
|
|
13
|
+
} from "@evolu/common";
|
|
14
|
+
|
|
15
|
+
/** {@link Time} with Node.js high-resolution nanosecond readings. */
|
|
16
|
+
export interface NodejsTime extends Time {
|
|
17
|
+
/** Returns a monotonic high-resolution timestamp in nanoseconds. */
|
|
18
|
+
readonly hrtime: () => HrTime;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Monotonic high-resolution timestamp in nanoseconds. */
|
|
22
|
+
export type HrTime = bigint & Brand<"HrTime">;
|
|
23
|
+
|
|
24
|
+
/** Elapsed nanoseconds measured using {@link HrTime}. */
|
|
25
|
+
export type HrDuration = bigint & Brand<"HrDuration">;
|
|
26
|
+
|
|
27
|
+
/** Creates a {@link NodejsTime} using `process.hrtime.bigint()`. */
|
|
28
|
+
export const createNodejsTime = (): NodejsTime => ({
|
|
29
|
+
...createTime(),
|
|
30
|
+
hrtime: () => process.hrtime.bigint() as HrTime,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns the elapsed nanoseconds between two high-resolution timestamps.
|
|
35
|
+
*
|
|
36
|
+
* Throws if `end` precedes `start`.
|
|
37
|
+
*/
|
|
38
|
+
export const hrDurationBetween = (start: HrTime, end: HrTime): HrDuration => {
|
|
39
|
+
assert(end >= start, "High-resolution end time must not precede start time");
|
|
40
|
+
return (end - start) as HrDuration;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Converts a high-resolution duration to the nearest millisecond. */
|
|
44
|
+
export const hrDurationToMillis = (duration: HrDuration): Millis =>
|
|
45
|
+
Millis.orThrow(
|
|
46
|
+
Number(
|
|
47
|
+
(duration + nanosecondsPerMillisecond / 2n) / nanosecondsPerMillisecond,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
/** Converts milliseconds to an exact high-resolution duration. */
|
|
52
|
+
export const millisToHrDuration = (millis: Millis): HrDuration =>
|
|
53
|
+
(BigInt(millis) * nanosecondsPerMillisecond) as HrDuration;
|
|
54
|
+
|
|
55
|
+
const nanosecondsPerMillisecond = 1_000_000n;
|
package/src/Worker.ts
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
import type { BroadcastChannel, CreateBroadcastChannel } from "@evolu/common";
|
|
2
|
+
import { disposable } from "@evolu/common";
|
|
3
|
+
|
|
4
|
+
/** Creates a {@link BroadcastChannel} from a Node.js BroadcastChannel. */
|
|
5
|
+
export const createBroadcastChannel: CreateBroadcastChannel = <
|
|
6
|
+
Input,
|
|
7
|
+
Output = Input,
|
|
8
|
+
>(
|
|
9
|
+
name: string,
|
|
10
|
+
): BroadcastChannel<Input, Output> => {
|
|
11
|
+
const nativeBroadcastChannel = new globalThis.BroadcastChannel(name);
|
|
12
|
+
using disposer = new DisposableStack();
|
|
13
|
+
let disposed = false;
|
|
14
|
+
|
|
15
|
+
disposer.defer(() => {
|
|
16
|
+
disposed = true;
|
|
17
|
+
nativeBroadcastChannel.onmessage = null;
|
|
18
|
+
nativeBroadcastChannel.close();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let onMessageHandler: ((message: Output) => void) | null = null;
|
|
22
|
+
|
|
23
|
+
return disposable<BroadcastChannel<Input, Output>>(
|
|
24
|
+
{
|
|
25
|
+
postMessage: (message) => {
|
|
26
|
+
nativeBroadcastChannel.postMessage(message);
|
|
27
|
+
},
|
|
28
|
+
get onMessage() {
|
|
29
|
+
return disposed ? null : onMessageHandler;
|
|
30
|
+
},
|
|
31
|
+
set onMessage(fn) {
|
|
32
|
+
if (disposed) return;
|
|
33
|
+
onMessageHandler = fn;
|
|
34
|
+
nativeBroadcastChannel.onmessage = fn
|
|
35
|
+
? (event: MessageEvent<Output>) => {
|
|
36
|
+
fn(event.data);
|
|
37
|
+
}
|
|
38
|
+
: null;
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
disposer,
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
1
45
|
// TODO: Implement Node.js Worker API
|
|
2
46
|
//
|
|
3
47
|
// This module should provide Node.js implementations of the common Worker API:
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* @mergeModuleWith <project>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from "./Cli.ts";
|
|
7
|
+
export * from "./Crypto.ts";
|
|
8
|
+
export * from "./local-first/Relay.ts";
|
|
9
|
+
export * from "./Platform.ts";
|
|
10
|
+
export * from "./Sqlite.ts";
|
|
11
|
+
export * from "./Task.ts";
|
|
12
|
+
export * from "./Time.ts";
|
|
13
|
+
export * from "./WebSocket.ts";
|
|
14
|
+
export * from "./Worker.ts";
|
package/src/local-first/Relay.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AbortError,
|
|
3
2
|
assert,
|
|
4
|
-
callback,
|
|
5
3
|
createRandom,
|
|
6
4
|
createRelation,
|
|
7
5
|
createSqlite,
|
|
8
6
|
type CreateSqliteDriverDep,
|
|
7
|
+
daemon,
|
|
9
8
|
Name,
|
|
10
9
|
ok,
|
|
11
10
|
OwnerId,
|
|
12
11
|
type RandomDep,
|
|
13
12
|
type Task,
|
|
14
13
|
type TimingSafeEqualDep,
|
|
14
|
+
tryAsync,
|
|
15
15
|
Uint8Array,
|
|
16
16
|
} from "@evolu/common";
|
|
17
17
|
import {
|
|
@@ -29,8 +29,8 @@ import { once } from "events";
|
|
|
29
29
|
import { existsSync } from "fs";
|
|
30
30
|
import { createServer } from "http";
|
|
31
31
|
import { WebSocket, WebSocketServer } from "ws";
|
|
32
|
-
import { createTimingSafeEqual } from "../Crypto.
|
|
33
|
-
import { createBetterSqliteDriver } from "../Sqlite.
|
|
32
|
+
import { createTimingSafeEqual } from "../Crypto.ts";
|
|
33
|
+
import { createBetterSqliteDriver } from "../Sqlite.ts";
|
|
34
34
|
|
|
35
35
|
export interface NodeJsRelayConfig extends RelayConfig {
|
|
36
36
|
/** The port number for the HTTP server. */
|
|
@@ -39,7 +39,7 @@ export interface NodeJsRelayConfig extends RelayConfig {
|
|
|
39
39
|
|
|
40
40
|
export type RelayDeps = CreateSqliteDriverDep & RandomDep & TimingSafeEqualDep;
|
|
41
41
|
|
|
42
|
-
/** Dependencies for {@link
|
|
42
|
+
/** Dependencies for {@link createRelay} using better-sqlite3. */
|
|
43
43
|
export const createRelayDeps = (): RelayDeps => ({
|
|
44
44
|
createSqliteDriver: createBetterSqliteDriver,
|
|
45
45
|
random: createRandom(),
|
|
@@ -47,7 +47,7 @@ export const createRelayDeps = (): RelayDeps => ({
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* Creates an Evolu Relay server resource using Node.js.
|
|
51
51
|
*
|
|
52
52
|
* Use {@link createRelayDeps} to create dependencies for better-sqlite3, or
|
|
53
53
|
* provide a custom SQLite driver implementation.
|
|
@@ -68,29 +68,22 @@ export const createRelayDeps = (): RelayDeps => ({
|
|
|
68
68
|
*
|
|
69
69
|
* const deps = { ...createRelayDeps(), console };
|
|
70
70
|
*
|
|
71
|
-
* await
|
|
72
|
-
*
|
|
71
|
+
* await runMain(deps)(
|
|
72
|
+
* createRelay({
|
|
73
|
+
* port: 4000,
|
|
73
74
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* startRelay({
|
|
77
|
-
* port: 4000,
|
|
75
|
+
* // Note: Relay requires URL in format ws://host:port?ownerId=<ownerId>
|
|
76
|
+
* // isOwnerAllowed: (_ownerId, { signal: _signal }) => true,
|
|
78
77
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* return requiredBytes <= maxBytes;
|
|
85
|
-
* },
|
|
86
|
-
* }),
|
|
87
|
-
* ),
|
|
78
|
+
* isOwnerWithinQuota: (_ownerId, requiredBytes) => {
|
|
79
|
+
* const maxBytes = 1024 * 1024; // 1MB
|
|
80
|
+
* return requiredBytes <= maxBytes;
|
|
81
|
+
* },
|
|
82
|
+
* }),
|
|
88
83
|
* );
|
|
89
|
-
*
|
|
90
|
-
* await run.deps.shutdown;
|
|
91
84
|
* ```
|
|
92
85
|
*/
|
|
93
|
-
export const
|
|
86
|
+
export const createRelay =
|
|
94
87
|
({
|
|
95
88
|
port = 443,
|
|
96
89
|
name = Name.orThrow("evolu-relay"),
|
|
@@ -98,15 +91,11 @@ export const startRelay =
|
|
|
98
91
|
isOwnerWithinQuota,
|
|
99
92
|
}: NodeJsRelayConfig): Task<Relay, never, RelayDeps> =>
|
|
100
93
|
async (run) => {
|
|
101
|
-
await using
|
|
94
|
+
await using disposer = new AsyncDisposableStack();
|
|
102
95
|
const console = run.deps.console.child("relay");
|
|
103
96
|
|
|
104
|
-
stack.defer(() => {
|
|
105
|
-
console.info("Shutdown complete");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
97
|
const dbFileExists = existsSync(`${name}.db`);
|
|
109
|
-
const sqlite =
|
|
98
|
+
const sqlite = disposer.use(await run.ok(createSqlite(name)));
|
|
110
99
|
const deps = { ...run.deps, sqlite };
|
|
111
100
|
|
|
112
101
|
if (!dbFileExists) {
|
|
@@ -114,12 +103,12 @@ export const startRelay =
|
|
|
114
103
|
createRelayStorageTables(deps);
|
|
115
104
|
}
|
|
116
105
|
|
|
117
|
-
const server =
|
|
106
|
+
const server = disposer.use(createServer());
|
|
118
107
|
server.once("close", () => {
|
|
119
108
|
console.info("HTTP server closed");
|
|
120
109
|
});
|
|
121
110
|
|
|
122
|
-
const wss =
|
|
111
|
+
const wss = disposer.adopt(
|
|
123
112
|
new WebSocketServer({
|
|
124
113
|
maxPayload: defaultProtocolMessageMaxSize,
|
|
125
114
|
noServer: true,
|
|
@@ -135,13 +124,8 @@ export const startRelay =
|
|
|
135
124
|
|
|
136
125
|
const ownerSocketRelation = createRelation<OwnerId, WebSocket>();
|
|
137
126
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
storage: createRelaySqliteStorage(deps)({
|
|
141
|
-
isOwnerWithinQuota,
|
|
142
|
-
}),
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
127
|
+
const storage = createRelaySqliteStorage(deps)({ isOwnerWithinQuota });
|
|
128
|
+
const relayRun = disposer.use(run.create({ storage }));
|
|
145
129
|
|
|
146
130
|
server.on("upgrade", (request, socket, head) => {
|
|
147
131
|
socket.on("error", console.debug);
|
|
@@ -180,33 +164,43 @@ export const startRelay =
|
|
|
180
164
|
return;
|
|
181
165
|
}
|
|
182
166
|
|
|
183
|
-
const authorizationFiber = relayRun(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
167
|
+
const authorizationFiber = relayRun.abortable(
|
|
168
|
+
// Use daemon because authorization can call an external service that
|
|
169
|
+
// ignores abort. The daemon runs in the root Run, so aborting the
|
|
170
|
+
// current Run does not make its Fiber wait for the service Promise to
|
|
171
|
+
// settle.
|
|
172
|
+
daemon(
|
|
173
|
+
async (run) =>
|
|
174
|
+
await tryAsync(
|
|
175
|
+
() => isOwnerAllowed(ownerId, { signal: run.signal }),
|
|
176
|
+
(error) => ({ type: "OwnerAuthorizationError", error }) as const,
|
|
177
|
+
),
|
|
178
|
+
),
|
|
190
179
|
);
|
|
191
180
|
|
|
192
181
|
const abortAuthorization = () => {
|
|
193
|
-
authorizationFiber.abort(
|
|
182
|
+
authorizationFiber.abort({
|
|
183
|
+
type: "WebSocketUpgradeSocketClosed",
|
|
184
|
+
});
|
|
194
185
|
};
|
|
195
186
|
|
|
196
187
|
socket.once("close", abortAuthorization);
|
|
197
188
|
socket.once("error", abortAuthorization);
|
|
198
189
|
|
|
199
|
-
void
|
|
190
|
+
void (async () => {
|
|
191
|
+
const result = await authorizationFiber;
|
|
192
|
+
|
|
200
193
|
socket.removeListener("close", abortAuthorization);
|
|
201
194
|
socket.removeListener("error", abortAuthorization);
|
|
202
195
|
|
|
203
196
|
if (!result.ok) {
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
respondAndDestroy(503);
|
|
197
|
+
if (result.error.type === "AbortError") {
|
|
198
|
+
socket.destroy();
|
|
207
199
|
return;
|
|
208
200
|
}
|
|
209
|
-
|
|
201
|
+
|
|
202
|
+
console.error(result.error.error);
|
|
203
|
+
respondAndDestroy(503);
|
|
210
204
|
return;
|
|
211
205
|
}
|
|
212
206
|
|
|
@@ -217,7 +211,7 @@ export const startRelay =
|
|
|
217
211
|
}
|
|
218
212
|
|
|
219
213
|
completeUpgrade();
|
|
220
|
-
});
|
|
214
|
+
})();
|
|
221
215
|
});
|
|
222
216
|
|
|
223
217
|
wss.on("connection", (ws) => {
|
|
@@ -261,13 +255,16 @@ export const startRelay =
|
|
|
261
255
|
if (!Uint8Array.is(message)) return;
|
|
262
256
|
|
|
263
257
|
void (async () => {
|
|
264
|
-
const response = await relayRun(
|
|
258
|
+
const response = await relayRun.abortable(
|
|
265
259
|
applyProtocolMessageAsRelay(message, options),
|
|
266
260
|
);
|
|
261
|
+
|
|
267
262
|
if (!response.ok) {
|
|
263
|
+
if (response.error.type === "AbortError") return;
|
|
268
264
|
console.error(response);
|
|
269
265
|
return;
|
|
270
266
|
}
|
|
267
|
+
|
|
271
268
|
ws.send(response.value.message, { binary: true });
|
|
272
269
|
})();
|
|
273
270
|
});
|
|
@@ -278,8 +275,7 @@ export const startRelay =
|
|
|
278
275
|
});
|
|
279
276
|
});
|
|
280
277
|
|
|
281
|
-
|
|
282
|
-
console.info("Shutting down...");
|
|
278
|
+
disposer.defer(() => {
|
|
283
279
|
for (const client of wss.clients) {
|
|
284
280
|
if (client.readyState === WebSocket.OPEN) {
|
|
285
281
|
client.close(1000, "Evolu Relay shutting down");
|
|
@@ -293,13 +289,13 @@ export const startRelay =
|
|
|
293
289
|
const address = server.address();
|
|
294
290
|
assert(address && typeof address !== "string", "Expected TCP address");
|
|
295
291
|
|
|
296
|
-
const
|
|
292
|
+
const disposables = disposer.move();
|
|
297
293
|
|
|
298
294
|
console.info(`Started on port ${address.port}`);
|
|
299
295
|
|
|
300
296
|
return ok({
|
|
301
297
|
port: address.port,
|
|
302
|
-
[Symbol.asyncDispose]: () =>
|
|
298
|
+
[Symbol.asyncDispose]: () => disposables.disposeAsync(),
|
|
303
299
|
});
|
|
304
300
|
};
|
|
305
301
|
|