@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.
@@ -1,2 +1,4 @@
1
- export {};
1
+ import type { CreateBroadcastChannel } from "@evolu/common";
2
+ /** Creates a {@link BroadcastChannel} from a Node.js BroadcastChannel. */
3
+ export declare const createBroadcastChannel: CreateBroadcastChannel;
2
4
  //# sourceMappingURL=Worker.d.ts.map
@@ -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"}
@@ -1,4 +1,96 @@
1
- export {};
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:
@@ -1,6 +1,14 @@
1
- export * from "./Sqlite.js";
2
- export * from "./Crypto.js";
3
- export * from "./local-first/Relay.js";
4
- export * from "./Task.js";
5
- export * from "./WebSocket.js";
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,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
- export * from "./Sqlite.js";
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 startRelay} using better-sqlite3. */
8
+ /** Dependencies for {@link createRelay} using better-sqlite3. */
9
9
  export declare const createRelayDeps: () => RelayDeps;
10
10
  /**
11
- * Starts an Evolu relay server using Node.js.
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 using run = createRun(deps);
33
- * await using stack = new AsyncDisposableStack();
32
+ * await runMain(deps)(
33
+ * createRelay({
34
+ * port: 4000,
34
35
  *
35
- * stack.use(
36
- * await run.orThrow(
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
- * // Note: Relay requires URL in format ws://host:port?ownerId=<ownerId>
41
- * // isOwnerAllowed: (_ownerId, { signal: _signal }) => true,
42
- *
43
- * isOwnerWithinQuota: (_ownerId, requiredBytes) => {
44
- * const maxBytes = 1024 * 1024; // 1MB
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 startRelay: ({ port, name, isOwnerAllowed, isOwnerWithinQuota, }: NodeJsRelayConfig) => Task<Relay, never, RelayDeps>;
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,EAOL,KAAK,qBAAqB,EAI1B,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,kBAAkB,EAExB,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,gEAAgE;AAChE,eAAO,MAAM,eAAe,QAAO,SAIjC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,UAAU,GACpB,qDAKE,iBAAiB,KAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CA6MlD,CAAC"}
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 { AbortError, assert, callback, createRandom, createRelation, createSqlite, Name, ok, OwnerId, Uint8Array, } from "@evolu/common";
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 startRelay} using better-sqlite3. */
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
- * Starts an Evolu relay server using Node.js.
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 using run = createRun(deps);
90
- * await using stack = new AsyncDisposableStack();
89
+ * await runMain(deps)(
90
+ * createRelay({
91
+ * port: 4000,
91
92
  *
92
- * stack.use(
93
- * await run.orThrow(
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
- * // Note: Relay requires URL in format ws://host:port?ownerId=<ownerId>
98
- * // isOwnerAllowed: (_ownerId, { signal: _signal }) => true,
99
- *
100
- * isOwnerWithinQuota: (_ownerId, requiredBytes) => {
101
- * const maxBytes = 1024 * 1024; // 1MB
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 startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isOwnerAllowed, isOwnerWithinQuota, }) => async (run) => {
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 stack = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
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 = stack.use(await run.orThrow(createSqlite(name)));
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 = stack.use(createServer());
116
+ const server = disposer.use(createServer());
127
117
  server.once("close", () => {
128
118
  console.info("HTTP server closed");
129
119
  });
130
- const wss = stack.adopt(new WebSocketServer({
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 relayRun = stack.use(run.create().addDeps({
141
- storage: createRelaySqliteStorage(deps)({
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(callback(({ ok, err, signal }) => {
173
- void Promise.try(() => isOwnerAllowed(ownerId, { signal })).then(ok, err);
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("WebSocket upgrade request socket closed");
166
+ authorizationFiber.abort({
167
+ type: "WebSocketUpgradeSocketClosed",
168
+ });
177
169
  };
178
170
  socket.once("close", abortAuthorization);
179
171
  socket.once("error", abortAuthorization);
180
- void authorizationFiber.then((result) => {
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 (!AbortError.is(result.error)) {
185
- console.error("isOwnerAllowed failed", ownerId, result.error);
186
- respondAndDestroy(503);
177
+ if (result.error.type === "AbortError") {
178
+ socket.destroy();
187
179
  return;
188
180
  }
189
- socket.destroy();
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
- stack.defer(() => {
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 moved = stack.move();
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]: () => moved.disposeAsync(),
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-next.2",
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.6.2",
42
+ "better-sqlite3": "^12.11.1",
31
43
  "ws": "^8.19.0"
32
44
  },
33
45
  "devDependencies": {
34
- "@types/better-sqlite3": "^7.6.13",
46
+ "@types/better-sqlite3": "^9.6.0",
35
47
  "@types/node": "^24.10.9",
36
48
  "@types/ws": "^8.18.1",
37
- "typescript": "^6",
38
- "@evolu/common": "8.0.0-next.2",
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
+ };
@@ -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 stack = new globalThis.DisposableStack();
15
- const db = stack.adopt(new BetterSQLite(filename), (db) => {
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 = stack.use(
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
- lazyVoid,
33
+ constVoid,
25
34
  ),
26
35
  );
27
36
 
28
- const driver: SqliteDriver = {
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
- [Symbol.dispose]: () => {
55
- stack.dispose();
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
- return ok(driver);
75
+ [Symbol.dispose]: () => {
76
+ disposables.dispose();
77
+ },
78
+ });
60
79
  };