@evolu/nodejs 3.0.0-next.3 → 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.js +2 -2
- 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.map +1 -1
- package/dist/src/Worker.js +7 -8
- package/dist/src/index.d.ts +13 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -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 +36 -43
- package/package.json +30 -6
- package/src/Cli.ts +78 -0
- package/src/Platform.ts +20 -0
- package/src/Sqlite.ts +2 -2
- package/src/Task.ts +172 -63
- package/src/TestBundle.ts +674 -0
- package/src/Time.ts +55 -0
- package/src/Worker.ts +22 -20
- package/src/index.ts +14 -6
- package/src/local-first/Relay.ts +48 -53
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,5 +1,5 @@
|
|
|
1
1
|
import type { BroadcastChannel, CreateBroadcastChannel } from "@evolu/common";
|
|
2
|
-
import {
|
|
2
|
+
import { disposable } from "@evolu/common";
|
|
3
3
|
|
|
4
4
|
/** Creates a {@link BroadcastChannel} from a Node.js BroadcastChannel. */
|
|
5
5
|
export const createBroadcastChannel: CreateBroadcastChannel = <
|
|
@@ -10,34 +10,36 @@ export const createBroadcastChannel: CreateBroadcastChannel = <
|
|
|
10
10
|
): BroadcastChannel<Input, Output> => {
|
|
11
11
|
const nativeBroadcastChannel = new globalThis.BroadcastChannel(name);
|
|
12
12
|
using disposer = new DisposableStack();
|
|
13
|
+
let disposed = false;
|
|
13
14
|
|
|
14
15
|
disposer.defer(() => {
|
|
16
|
+
disposed = true;
|
|
15
17
|
nativeBroadcastChannel.onmessage = null;
|
|
16
18
|
nativeBroadcastChannel.close();
|
|
17
19
|
});
|
|
18
20
|
|
|
19
|
-
const disposables = disposer.move();
|
|
20
21
|
let onMessageHandler: ((message: Output) => void) | null = null;
|
|
21
22
|
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
},
|
|
26
40
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
},
|
|
30
|
-
set onMessage(fn) {
|
|
31
|
-
if (disposables.disposed) return;
|
|
32
|
-
onMessageHandler = fn;
|
|
33
|
-
nativeBroadcastChannel.onmessage = fn
|
|
34
|
-
? (event: MessageEvent<Output>) => {
|
|
35
|
-
fn(event.data);
|
|
36
|
-
}
|
|
37
|
-
: null;
|
|
38
|
-
},
|
|
39
|
-
[Symbol.dispose]: () => disposables.dispose(),
|
|
40
|
-
};
|
|
41
|
+
disposer,
|
|
42
|
+
);
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
// TODO: Implement Node.js Worker API
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export * from "./
|
|
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"),
|
|
@@ -101,12 +94,8 @@ export const startRelay =
|
|
|
101
94
|
await using disposer = new AsyncDisposableStack();
|
|
102
95
|
const console = run.deps.console.child("relay");
|
|
103
96
|
|
|
104
|
-
disposer.defer(() => {
|
|
105
|
-
console.info("Shutdown complete");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
97
|
const dbFileExists = existsSync(`${name}.db`);
|
|
109
|
-
const sqlite = disposer.use(await run.
|
|
98
|
+
const sqlite = disposer.use(await run.ok(createSqlite(name)));
|
|
110
99
|
const deps = { ...run.deps, sqlite };
|
|
111
100
|
|
|
112
101
|
if (!dbFileExists) {
|
|
@@ -135,14 +124,8 @@ export const startRelay =
|
|
|
135
124
|
|
|
136
125
|
const ownerSocketRelation = createRelation<OwnerId, WebSocket>();
|
|
137
126
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
...run.deps,
|
|
141
|
-
storage: createRelaySqliteStorage(deps)({
|
|
142
|
-
isOwnerWithinQuota,
|
|
143
|
-
}),
|
|
144
|
-
}),
|
|
145
|
-
);
|
|
127
|
+
const storage = createRelaySqliteStorage(deps)({ isOwnerWithinQuota });
|
|
128
|
+
const relayRun = disposer.use(run.create({ storage }));
|
|
146
129
|
|
|
147
130
|
server.on("upgrade", (request, socket, head) => {
|
|
148
131
|
socket.on("error", console.debug);
|
|
@@ -181,33 +164,43 @@ export const startRelay =
|
|
|
181
164
|
return;
|
|
182
165
|
}
|
|
183
166
|
|
|
184
|
-
const authorizationFiber = relayRun(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
),
|
|
191
179
|
);
|
|
192
180
|
|
|
193
181
|
const abortAuthorization = () => {
|
|
194
|
-
authorizationFiber.abort(
|
|
182
|
+
authorizationFiber.abort({
|
|
183
|
+
type: "WebSocketUpgradeSocketClosed",
|
|
184
|
+
});
|
|
195
185
|
};
|
|
196
186
|
|
|
197
187
|
socket.once("close", abortAuthorization);
|
|
198
188
|
socket.once("error", abortAuthorization);
|
|
199
189
|
|
|
200
|
-
void
|
|
190
|
+
void (async () => {
|
|
191
|
+
const result = await authorizationFiber;
|
|
192
|
+
|
|
201
193
|
socket.removeListener("close", abortAuthorization);
|
|
202
194
|
socket.removeListener("error", abortAuthorization);
|
|
203
195
|
|
|
204
196
|
if (!result.ok) {
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
respondAndDestroy(503);
|
|
197
|
+
if (result.error.type === "AbortError") {
|
|
198
|
+
socket.destroy();
|
|
208
199
|
return;
|
|
209
200
|
}
|
|
210
|
-
|
|
201
|
+
|
|
202
|
+
console.error(result.error.error);
|
|
203
|
+
respondAndDestroy(503);
|
|
211
204
|
return;
|
|
212
205
|
}
|
|
213
206
|
|
|
@@ -218,7 +211,7 @@ export const startRelay =
|
|
|
218
211
|
}
|
|
219
212
|
|
|
220
213
|
completeUpgrade();
|
|
221
|
-
});
|
|
214
|
+
})();
|
|
222
215
|
});
|
|
223
216
|
|
|
224
217
|
wss.on("connection", (ws) => {
|
|
@@ -262,13 +255,16 @@ export const startRelay =
|
|
|
262
255
|
if (!Uint8Array.is(message)) return;
|
|
263
256
|
|
|
264
257
|
void (async () => {
|
|
265
|
-
const response = await relayRun(
|
|
258
|
+
const response = await relayRun.abortable(
|
|
266
259
|
applyProtocolMessageAsRelay(message, options),
|
|
267
260
|
);
|
|
261
|
+
|
|
268
262
|
if (!response.ok) {
|
|
263
|
+
if (response.error.type === "AbortError") return;
|
|
269
264
|
console.error(response);
|
|
270
265
|
return;
|
|
271
266
|
}
|
|
267
|
+
|
|
272
268
|
ws.send(response.value.message, { binary: true });
|
|
273
269
|
})();
|
|
274
270
|
});
|
|
@@ -280,7 +276,6 @@ export const startRelay =
|
|
|
280
276
|
});
|
|
281
277
|
|
|
282
278
|
disposer.defer(() => {
|
|
283
|
-
console.info("Shutting down...");
|
|
284
279
|
for (const client of wss.clients) {
|
|
285
280
|
if (client.readyState === WebSocket.OPEN) {
|
|
286
281
|
client.close(1000, "Evolu Relay shutting down");
|