@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/dist/src/Worker.d.ts
CHANGED
package/dist/src/Worker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG9E,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,EAAE,sBAsCpC,CAAC"}
|
package/dist/src/Worker.js
CHANGED
|
@@ -1,4 +1,96 @@
|
|
|
1
|
-
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { disposable } from "@evolu/common";
|
|
54
|
+
/** Creates a {@link BroadcastChannel} from a Node.js BroadcastChannel. */
|
|
55
|
+
export const createBroadcastChannel = (name) => {
|
|
56
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
57
|
+
try {
|
|
58
|
+
const nativeBroadcastChannel = new globalThis.BroadcastChannel(name);
|
|
59
|
+
const disposer = __addDisposableResource(env_1, new DisposableStack(), false);
|
|
60
|
+
let disposed = false;
|
|
61
|
+
disposer.defer(() => {
|
|
62
|
+
disposed = true;
|
|
63
|
+
nativeBroadcastChannel.onmessage = null;
|
|
64
|
+
nativeBroadcastChannel.close();
|
|
65
|
+
});
|
|
66
|
+
let onMessageHandler = null;
|
|
67
|
+
return disposable({
|
|
68
|
+
postMessage: (message) => {
|
|
69
|
+
nativeBroadcastChannel.postMessage(message);
|
|
70
|
+
},
|
|
71
|
+
get onMessage() {
|
|
72
|
+
return disposed ? null : onMessageHandler;
|
|
73
|
+
},
|
|
74
|
+
set onMessage(fn) {
|
|
75
|
+
if (disposed)
|
|
76
|
+
return;
|
|
77
|
+
onMessageHandler = fn;
|
|
78
|
+
nativeBroadcastChannel.onmessage = fn
|
|
79
|
+
? (event) => {
|
|
80
|
+
fn(event.data);
|
|
81
|
+
}
|
|
82
|
+
: null;
|
|
83
|
+
},
|
|
84
|
+
}, disposer);
|
|
85
|
+
}
|
|
86
|
+
catch (e_1) {
|
|
87
|
+
env_1.error = e_1;
|
|
88
|
+
env_1.hasError = true;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
__disposeResources(env_1);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
2
94
|
// TODO: Implement Node.js Worker API
|
|
3
95
|
//
|
|
4
96
|
// This module should provide Node.js implementations of the common Worker API:
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export * from "./
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* @mergeModuleWith <project>
|
|
4
|
+
*/
|
|
5
|
+
export * from "./Cli.ts";
|
|
6
|
+
export * from "./Crypto.ts";
|
|
7
|
+
export * from "./local-first/Relay.ts";
|
|
8
|
+
export * from "./Platform.ts";
|
|
9
|
+
export * from "./Sqlite.ts";
|
|
10
|
+
export * from "./Task.ts";
|
|
11
|
+
export * from "./Time.ts";
|
|
12
|
+
export * from "./WebSocket.ts";
|
|
13
|
+
export * from "./Worker.ts";
|
|
6
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* @mergeModuleWith <project>
|
|
4
|
+
*/
|
|
5
|
+
export * from "./Cli.js";
|
|
2
6
|
export * from "./Crypto.js";
|
|
3
7
|
export * from "./local-first/Relay.js";
|
|
8
|
+
export * from "./Platform.js";
|
|
9
|
+
export * from "./Sqlite.js";
|
|
4
10
|
export * from "./Task.js";
|
|
11
|
+
export * from "./Time.js";
|
|
5
12
|
export * from "./WebSocket.js";
|
|
13
|
+
export * from "./Worker.js";
|
|
@@ -5,10 +5,10 @@ export interface NodeJsRelayConfig extends RelayConfig {
|
|
|
5
5
|
readonly port?: number;
|
|
6
6
|
}
|
|
7
7
|
export type RelayDeps = CreateSqliteDriverDep & RandomDep & TimingSafeEqualDep;
|
|
8
|
-
/** Dependencies for {@link
|
|
8
|
+
/** Dependencies for {@link createRelay} using better-sqlite3. */
|
|
9
9
|
export declare const createRelayDeps: () => RelayDeps;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Creates an Evolu Relay server resource using Node.js.
|
|
12
12
|
*
|
|
13
13
|
* Use {@link createRelayDeps} to create dependencies for better-sqlite3, or
|
|
14
14
|
* provide a custom SQLite driver implementation.
|
|
@@ -29,27 +29,20 @@ export declare const createRelayDeps: () => RelayDeps;
|
|
|
29
29
|
*
|
|
30
30
|
* const deps = { ...createRelayDeps(), console };
|
|
31
31
|
*
|
|
32
|
-
* await
|
|
33
|
-
*
|
|
32
|
+
* await runMain(deps)(
|
|
33
|
+
* createRelay({
|
|
34
|
+
* port: 4000,
|
|
34
35
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* startRelay({
|
|
38
|
-
* port: 4000,
|
|
36
|
+
* // Note: Relay requires URL in format ws://host:port?ownerId=<ownerId>
|
|
37
|
+
* // isOwnerAllowed: (_ownerId, { signal: _signal }) => true,
|
|
39
38
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* return requiredBytes <= maxBytes;
|
|
46
|
-
* },
|
|
47
|
-
* }),
|
|
48
|
-
* ),
|
|
39
|
+
* isOwnerWithinQuota: (_ownerId, requiredBytes) => {
|
|
40
|
+
* const maxBytes = 1024 * 1024; // 1MB
|
|
41
|
+
* return requiredBytes <= maxBytes;
|
|
42
|
+
* },
|
|
43
|
+
* }),
|
|
49
44
|
* );
|
|
50
|
-
*
|
|
51
|
-
* await run.deps.shutdown;
|
|
52
45
|
* ```
|
|
53
46
|
*/
|
|
54
|
-
export declare const
|
|
47
|
+
export declare const createRelay: ({ port, name, isOwnerAllowed, isOwnerWithinQuota, }: NodeJsRelayConfig) => Task<Relay, never, RelayDeps>;
|
|
55
48
|
//# sourceMappingURL=Relay.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/local-first/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/local-first/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,qBAAqB,EAK1B,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,kBAAkB,EAGxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAQL,KAAK,KAAK,EACV,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAQnC,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,qBAAqB,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAE/E,iEAAiE;AACjE,eAAO,MAAM,eAAe,QAAO,SAIjC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,wDAMnB,iBAAiB,KAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAgNlD,CAAC"}
|
|
@@ -50,7 +50,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
-
import {
|
|
53
|
+
import { assert, createRandom, createRelation, createSqlite, daemon, Name, ok, OwnerId, tryAsync, Uint8Array, } from "@evolu/common";
|
|
54
54
|
import { applyProtocolMessageAsRelay, createBaseSqliteStorageTables, createRelaySqliteStorage, createRelayStorageTables, defaultProtocolMessageMaxSize, parseOwnerIdFromOwnerWebSocketTransportUrl, } from "@evolu/common/local-first";
|
|
55
55
|
import { once } from "events";
|
|
56
56
|
import { existsSync } from "fs";
|
|
@@ -58,14 +58,14 @@ import { createServer } from "http";
|
|
|
58
58
|
import { WebSocket, WebSocketServer } from "ws";
|
|
59
59
|
import { createTimingSafeEqual } from "../Crypto.js";
|
|
60
60
|
import { createBetterSqliteDriver } from "../Sqlite.js";
|
|
61
|
-
/** Dependencies for {@link
|
|
61
|
+
/** Dependencies for {@link createRelay} using better-sqlite3. */
|
|
62
62
|
export const createRelayDeps = () => ({
|
|
63
63
|
createSqliteDriver: createBetterSqliteDriver,
|
|
64
64
|
random: createRandom(),
|
|
65
65
|
timingSafeEqual: createTimingSafeEqual(),
|
|
66
66
|
});
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
68
|
+
* Creates an Evolu Relay server resource using Node.js.
|
|
69
69
|
*
|
|
70
70
|
* Use {@link createRelayDeps} to create dependencies for better-sqlite3, or
|
|
71
71
|
* provide a custom SQLite driver implementation.
|
|
@@ -86,48 +86,38 @@ export const createRelayDeps = () => ({
|
|
|
86
86
|
*
|
|
87
87
|
* const deps = { ...createRelayDeps(), console };
|
|
88
88
|
*
|
|
89
|
-
* await
|
|
90
|
-
*
|
|
89
|
+
* await runMain(deps)(
|
|
90
|
+
* createRelay({
|
|
91
|
+
* port: 4000,
|
|
91
92
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* startRelay({
|
|
95
|
-
* port: 4000,
|
|
93
|
+
* // Note: Relay requires URL in format ws://host:port?ownerId=<ownerId>
|
|
94
|
+
* // isOwnerAllowed: (_ownerId, { signal: _signal }) => true,
|
|
96
95
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* return requiredBytes <= maxBytes;
|
|
103
|
-
* },
|
|
104
|
-
* }),
|
|
105
|
-
* ),
|
|
96
|
+
* isOwnerWithinQuota: (_ownerId, requiredBytes) => {
|
|
97
|
+
* const maxBytes = 1024 * 1024; // 1MB
|
|
98
|
+
* return requiredBytes <= maxBytes;
|
|
99
|
+
* },
|
|
100
|
+
* }),
|
|
106
101
|
* );
|
|
107
|
-
*
|
|
108
|
-
* await run.deps.shutdown;
|
|
109
102
|
* ```
|
|
110
103
|
*/
|
|
111
|
-
export const
|
|
104
|
+
export const createRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isOwnerAllowed, isOwnerWithinQuota, }) => async (run) => {
|
|
112
105
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
113
106
|
try {
|
|
114
|
-
const
|
|
107
|
+
const disposer = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
|
|
115
108
|
const console = run.deps.console.child("relay");
|
|
116
|
-
stack.defer(() => {
|
|
117
|
-
console.info("Shutdown complete");
|
|
118
|
-
});
|
|
119
109
|
const dbFileExists = existsSync(`${name}.db`);
|
|
120
|
-
const sqlite =
|
|
110
|
+
const sqlite = disposer.use(await run.ok(createSqlite(name)));
|
|
121
111
|
const deps = { ...run.deps, sqlite };
|
|
122
112
|
if (!dbFileExists) {
|
|
123
113
|
createBaseSqliteStorageTables(deps);
|
|
124
114
|
createRelayStorageTables(deps);
|
|
125
115
|
}
|
|
126
|
-
const server =
|
|
116
|
+
const server = disposer.use(createServer());
|
|
127
117
|
server.once("close", () => {
|
|
128
118
|
console.info("HTTP server closed");
|
|
129
119
|
});
|
|
130
|
-
const wss =
|
|
120
|
+
const wss = disposer.adopt(new WebSocketServer({
|
|
131
121
|
maxPayload: defaultProtocolMessageMaxSize,
|
|
132
122
|
noServer: true,
|
|
133
123
|
}), (wss) => new Promise((resolve) => {
|
|
@@ -137,11 +127,8 @@ export const startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isO
|
|
|
137
127
|
});
|
|
138
128
|
}));
|
|
139
129
|
const ownerSocketRelation = createRelation();
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
isOwnerWithinQuota,
|
|
143
|
-
}),
|
|
144
|
-
}));
|
|
130
|
+
const storage = createRelaySqliteStorage(deps)({ isOwnerWithinQuota });
|
|
131
|
+
const relayRun = disposer.use(run.create({ storage }));
|
|
145
132
|
server.on("upgrade", (request, socket, head) => {
|
|
146
133
|
socket.on("error", console.debug);
|
|
147
134
|
const completeUpgrade = () => {
|
|
@@ -169,24 +156,30 @@ export const startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isO
|
|
|
169
156
|
respondAndDestroy(400);
|
|
170
157
|
return;
|
|
171
158
|
}
|
|
172
|
-
const authorizationFiber = relayRun(
|
|
173
|
-
|
|
174
|
-
|
|
159
|
+
const authorizationFiber = relayRun.abortable(
|
|
160
|
+
// Use daemon because authorization can call an external service that
|
|
161
|
+
// ignores abort. The daemon runs in the root Run, so aborting the
|
|
162
|
+
// current Run does not make its Fiber wait for the service Promise to
|
|
163
|
+
// settle.
|
|
164
|
+
daemon(async (run) => await tryAsync(() => isOwnerAllowed(ownerId, { signal: run.signal }), (error) => ({ type: "OwnerAuthorizationError", error }))));
|
|
175
165
|
const abortAuthorization = () => {
|
|
176
|
-
authorizationFiber.abort(
|
|
166
|
+
authorizationFiber.abort({
|
|
167
|
+
type: "WebSocketUpgradeSocketClosed",
|
|
168
|
+
});
|
|
177
169
|
};
|
|
178
170
|
socket.once("close", abortAuthorization);
|
|
179
171
|
socket.once("error", abortAuthorization);
|
|
180
|
-
void
|
|
172
|
+
void (async () => {
|
|
173
|
+
const result = await authorizationFiber;
|
|
181
174
|
socket.removeListener("close", abortAuthorization);
|
|
182
175
|
socket.removeListener("error", abortAuthorization);
|
|
183
176
|
if (!result.ok) {
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
respondAndDestroy(503);
|
|
177
|
+
if (result.error.type === "AbortError") {
|
|
178
|
+
socket.destroy();
|
|
187
179
|
return;
|
|
188
180
|
}
|
|
189
|
-
|
|
181
|
+
console.error(result.error.error);
|
|
182
|
+
respondAndDestroy(503);
|
|
190
183
|
return;
|
|
191
184
|
}
|
|
192
185
|
if (!result.value) {
|
|
@@ -195,7 +188,7 @@ export const startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isO
|
|
|
195
188
|
return;
|
|
196
189
|
}
|
|
197
190
|
completeUpgrade();
|
|
198
|
-
});
|
|
191
|
+
})();
|
|
199
192
|
});
|
|
200
193
|
wss.on("connection", (ws) => {
|
|
201
194
|
console.debug("on connection", wss.clients.size);
|
|
@@ -221,8 +214,10 @@ export const startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isO
|
|
|
221
214
|
if (!Uint8Array.is(message))
|
|
222
215
|
return;
|
|
223
216
|
void (async () => {
|
|
224
|
-
const response = await relayRun(applyProtocolMessageAsRelay(message, options));
|
|
217
|
+
const response = await relayRun.abortable(applyProtocolMessageAsRelay(message, options));
|
|
225
218
|
if (!response.ok) {
|
|
219
|
+
if (response.error.type === "AbortError")
|
|
220
|
+
return;
|
|
226
221
|
console.error(response);
|
|
227
222
|
return;
|
|
228
223
|
}
|
|
@@ -234,8 +229,7 @@ export const startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isO
|
|
|
234
229
|
console.debug("ws close", wss.clients.size);
|
|
235
230
|
});
|
|
236
231
|
});
|
|
237
|
-
|
|
238
|
-
console.info("Shutting down...");
|
|
232
|
+
disposer.defer(() => {
|
|
239
233
|
for (const client of wss.clients) {
|
|
240
234
|
if (client.readyState === WebSocket.OPEN) {
|
|
241
235
|
client.close(1000, "Evolu Relay shutting down");
|
|
@@ -246,11 +240,11 @@ export const startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isO
|
|
|
246
240
|
await once(server, "listening");
|
|
247
241
|
const address = server.address();
|
|
248
242
|
assert(address && typeof address !== "string", "Expected TCP address");
|
|
249
|
-
const
|
|
243
|
+
const disposables = disposer.move();
|
|
250
244
|
console.info(`Started on port ${address.port}`);
|
|
251
245
|
return ok({
|
|
252
246
|
port: address.port,
|
|
253
|
-
[Symbol.asyncDispose]: () =>
|
|
247
|
+
[Symbol.asyncDispose]: () => disposables.disposeAsync(),
|
|
254
248
|
});
|
|
255
249
|
}
|
|
256
250
|
catch (e_1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evolu/nodejs",
|
|
3
|
-
"version": "3.0.0
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Evolu for Node.js",
|
|
5
5
|
"author": "Daniel Steigerwald <daniel@steigerwald.cz>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,18 @@
|
|
|
19
19
|
"types": "./dist/src/index.d.ts",
|
|
20
20
|
"import": "./dist/src/index.js",
|
|
21
21
|
"default": "./dist/src/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./TestBundle": {
|
|
24
|
+
"types": "./dist/src/TestBundle.d.ts",
|
|
25
|
+
"import": "./dist/src/TestBundle.js",
|
|
26
|
+
"default": "./dist/src/TestBundle.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"typesVersions": {
|
|
30
|
+
"*": {
|
|
31
|
+
"TestBundle": [
|
|
32
|
+
"./dist/src/TestBundle.d.ts"
|
|
33
|
+
]
|
|
22
34
|
}
|
|
23
35
|
},
|
|
24
36
|
"files": [
|
|
@@ -27,19 +39,31 @@
|
|
|
27
39
|
"README.md"
|
|
28
40
|
],
|
|
29
41
|
"dependencies": {
|
|
30
|
-
"better-sqlite3": "^12.
|
|
42
|
+
"better-sqlite3": "^12.11.1",
|
|
31
43
|
"ws": "^8.19.0"
|
|
32
44
|
},
|
|
33
45
|
"devDependencies": {
|
|
34
|
-
"@types/better-sqlite3": "^
|
|
46
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
35
47
|
"@types/node": "^24.10.9",
|
|
36
48
|
"@types/ws": "^8.18.1",
|
|
37
|
-
"typescript": "
|
|
38
|
-
"
|
|
49
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
50
|
+
"vite": "^8.2.0",
|
|
51
|
+
"webpack": "^5.109.2",
|
|
52
|
+
"@evolu/common": "8.0.0",
|
|
39
53
|
"@evolu/typescript-config": "0.0.2"
|
|
40
54
|
},
|
|
41
55
|
"peerDependencies": {
|
|
42
|
-
"@evolu/common": "^8.0.0-next.0"
|
|
56
|
+
"@evolu/common": "^8.0.0-next.0",
|
|
57
|
+
"vite": ">=8.0.0",
|
|
58
|
+
"webpack": ">=5.108.4"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"vite": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"webpack": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
43
67
|
},
|
|
44
68
|
"engines": {
|
|
45
69
|
"node": ">=24.0.0"
|
package/src/Cli.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abort-aware CLI process utilities.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { callback, err, ok, type Task, type Typed } from "@evolu/common";
|
|
8
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
/** Runs a command with inherited stdio. */
|
|
11
|
+
export type Spawn = (
|
|
12
|
+
file: string,
|
|
13
|
+
args: ReadonlyArray<string>,
|
|
14
|
+
options?: {
|
|
15
|
+
readonly cwd?: string | URL;
|
|
16
|
+
},
|
|
17
|
+
) => Task<void, SpawnError>;
|
|
18
|
+
|
|
19
|
+
/** Failure to start a command or an unsuccessful command exit. */
|
|
20
|
+
export interface SpawnError extends Typed<"SpawnError"> {
|
|
21
|
+
readonly command: string;
|
|
22
|
+
readonly exitCode: number | null;
|
|
23
|
+
readonly signal: NodeJS.Signals | null;
|
|
24
|
+
readonly message: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Dependency wrapper for {@link spawn}. */
|
|
28
|
+
export interface SpawnDep {
|
|
29
|
+
readonly spawn: Spawn;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Runs a command with inherited stdio and aborts it with the current Run.
|
|
34
|
+
*
|
|
35
|
+
* A zero exit code succeeds. A start failure, non-zero exit code, or signal
|
|
36
|
+
* exit returns {@link SpawnError}.
|
|
37
|
+
*/
|
|
38
|
+
export const spawn: Spawn = (file, args, { cwd } = {}) => {
|
|
39
|
+
const command = [file, ...args].join(" ");
|
|
40
|
+
|
|
41
|
+
return callback(({ run, resolve }) => {
|
|
42
|
+
const child = nodeSpawn(file, args, {
|
|
43
|
+
cwd,
|
|
44
|
+
signal: run.signal,
|
|
45
|
+
stdio: "inherit",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
child.once("error", (error) => {
|
|
49
|
+
if (run.signal.aborted) return;
|
|
50
|
+
resolve(
|
|
51
|
+
err({
|
|
52
|
+
type: "SpawnError",
|
|
53
|
+
command,
|
|
54
|
+
exitCode: null,
|
|
55
|
+
signal: null,
|
|
56
|
+
message: `Failed to start ${command}: ${error.message}`,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
child.once("close", (exitCode, signal) => {
|
|
61
|
+
if (run.signal.aborted) return;
|
|
62
|
+
resolve(
|
|
63
|
+
exitCode === 0
|
|
64
|
+
? ok()
|
|
65
|
+
: err({
|
|
66
|
+
type: "SpawnError",
|
|
67
|
+
command,
|
|
68
|
+
exitCode,
|
|
69
|
+
signal,
|
|
70
|
+
message:
|
|
71
|
+
signal == null
|
|
72
|
+
? `${command} exited with code ${exitCode}.`
|
|
73
|
+
: `${command} exited from ${signal}.`,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
};
|
package/src/Platform.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js platform utilities.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { PositiveInt } from "@evolu/common";
|
|
8
|
+
import { availableParallelism as nodeAvailableParallelism } from "node:os";
|
|
9
|
+
|
|
10
|
+
/** Returns the recommended amount of parallelism available to this process. */
|
|
11
|
+
export type AvailableParallelism = () => PositiveInt;
|
|
12
|
+
|
|
13
|
+
/** Dependency wrapper for {@link availableParallelism}. */
|
|
14
|
+
export interface AvailableParallelismDep {
|
|
15
|
+
readonly availableParallelism: AvailableParallelism;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Returns the recommended amount of parallelism available to this process. */
|
|
19
|
+
export const availableParallelism: AvailableParallelism = () =>
|
|
20
|
+
PositiveInt.orThrow(nodeAvailableParallelism());
|
package/src/Sqlite.ts
CHANGED
|
@@ -1,31 +1,42 @@
|
|
|
1
1
|
import {
|
|
2
|
+
constVoid,
|
|
2
3
|
createPreparedStatementsCache,
|
|
3
|
-
lazyVoid,
|
|
4
4
|
ok,
|
|
5
5
|
type CreateSqliteDriver,
|
|
6
|
-
type SqliteDriver,
|
|
7
6
|
type SqliteRow,
|
|
8
7
|
} from "@evolu/common";
|
|
9
8
|
import BetterSQLite, { type Statement } from "better-sqlite3";
|
|
9
|
+
import { rmSync } from "fs";
|
|
10
10
|
|
|
11
11
|
export const createBetterSqliteDriver: CreateSqliteDriver =
|
|
12
12
|
(name, options) => () => {
|
|
13
13
|
const filename = options?.mode === "memory" ? ":memory:" : `${name}.db`;
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const filenamesToDelete =
|
|
15
|
+
options?.mode === "memory"
|
|
16
|
+
? []
|
|
17
|
+
: [
|
|
18
|
+
filename,
|
|
19
|
+
`${filename}-shm`,
|
|
20
|
+
`${filename}-wal`,
|
|
21
|
+
`${filename}-journal`,
|
|
22
|
+
];
|
|
23
|
+
using disposer = new DisposableStack();
|
|
24
|
+
const db = disposer.adopt(new BetterSQLite(filename), (db) => {
|
|
16
25
|
db.close();
|
|
17
26
|
});
|
|
18
27
|
|
|
19
|
-
const cache =
|
|
28
|
+
const cache = disposer.use(
|
|
20
29
|
createPreparedStatementsCache<Statement>(
|
|
21
30
|
(sql) => db.prepare(sql),
|
|
22
31
|
// Not needed.
|
|
23
32
|
// https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#class-statement
|
|
24
|
-
|
|
33
|
+
constVoid,
|
|
25
34
|
),
|
|
26
35
|
);
|
|
27
36
|
|
|
28
|
-
const
|
|
37
|
+
const disposables = disposer.move();
|
|
38
|
+
|
|
39
|
+
return ok({
|
|
29
40
|
exec: (query) => {
|
|
30
41
|
// Always prepare is recommended for better-sqlite3
|
|
31
42
|
const prepared = cache.get(query, true);
|
|
@@ -51,10 +62,18 @@ export const createBetterSqliteDriver: CreateSqliteDriver =
|
|
|
51
62
|
return new Uint8Array(file);
|
|
52
63
|
},
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
deleteDatabase: () => {
|
|
66
|
+
using deleteDisposer = new DisposableStack();
|
|
67
|
+
for (const filename of filenamesToDelete) {
|
|
68
|
+
deleteDisposer.defer(() => {
|
|
69
|
+
rmSync(filename, { force: true });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
deleteDisposer.use(disposables);
|
|
56
73
|
},
|
|
57
|
-
};
|
|
58
74
|
|
|
59
|
-
|
|
75
|
+
[Symbol.dispose]: () => {
|
|
76
|
+
disposables.dispose();
|
|
77
|
+
},
|
|
78
|
+
});
|
|
60
79
|
};
|