@evolu/nodejs 3.0.0-next.2 → 3.0.0-next.3

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 +1 @@
1
- {"version":3,"file":"Sqlite.d.ts","sourceRoot":"","sources":["../../src/Sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,kBAAkB,EAGxB,MAAM,eAAe,CAAC;AAGvB,eAAO,MAAM,wBAAwB,EAAE,kBAiDpC,CAAC"}
1
+ {"version":3,"file":"Sqlite.d.ts","sourceRoot":"","sources":["../../src/Sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,kBAAkB,EAExB,MAAM,eAAe,CAAC;AAIvB,eAAO,MAAM,wBAAwB,EAAE,kBAoEpC,CAAC"}
@@ -1,38 +1,128 @@
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
+ });
1
53
  import { createPreparedStatementsCache, lazyVoid, ok, } from "@evolu/common";
2
54
  import BetterSQLite, {} from "better-sqlite3";
55
+ import { rmSync } from "fs";
3
56
  export const createBetterSqliteDriver = (name, options) => () => {
4
- const filename = options?.mode === "memory" ? ":memory:" : `${name}.db`;
5
- const stack = new globalThis.DisposableStack();
6
- const db = stack.adopt(new BetterSQLite(filename), (db) => {
7
- db.close();
8
- });
9
- const cache = stack.use(createPreparedStatementsCache((sql) => db.prepare(sql),
10
- // Not needed.
11
- // https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#class-statement
12
- lazyVoid));
13
- const driver = {
14
- exec: (query) => {
15
- // Always prepare is recommended for better-sqlite3
16
- const prepared = cache.get(query, true);
17
- if (prepared.reader) {
18
- const rows = prepared.all(query.parameters);
19
- return { rows, changes: 0 };
20
- }
21
- const changes = prepared.run(query.parameters).changes;
22
- return { rows: [], changes };
23
- },
24
- export: () => {
25
- const file = db.serialize();
26
- const { buffer } = file;
27
- if (buffer instanceof ArrayBuffer) {
28
- return new Uint8Array(buffer, file.byteOffset, file.byteLength);
29
- }
30
- // Ensure export uses transferable ArrayBuffer backing.
31
- return new Uint8Array(file);
32
- },
33
- [Symbol.dispose]: () => {
34
- stack.dispose();
35
- },
36
- };
37
- return ok(driver);
57
+ const env_1 = { stack: [], error: void 0, hasError: false };
58
+ try {
59
+ const filename = options?.mode === "memory" ? ":memory:" : `${name}.db`;
60
+ const filenamesToDelete = options?.mode === "memory"
61
+ ? []
62
+ : [
63
+ filename,
64
+ `${filename}-shm`,
65
+ `${filename}-wal`,
66
+ `${filename}-journal`,
67
+ ];
68
+ const disposer = __addDisposableResource(env_1, new DisposableStack(), false);
69
+ const db = disposer.adopt(new BetterSQLite(filename), (db) => {
70
+ db.close();
71
+ });
72
+ const cache = disposer.use(createPreparedStatementsCache((sql) => db.prepare(sql),
73
+ // Not needed.
74
+ // https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#class-statement
75
+ lazyVoid));
76
+ const disposables = disposer.move();
77
+ return ok({
78
+ exec: (query) => {
79
+ // Always prepare is recommended for better-sqlite3
80
+ const prepared = cache.get(query, true);
81
+ if (prepared.reader) {
82
+ const rows = prepared.all(query.parameters);
83
+ return { rows, changes: 0 };
84
+ }
85
+ const changes = prepared.run(query.parameters).changes;
86
+ return { rows: [], changes };
87
+ },
88
+ export: () => {
89
+ const file = db.serialize();
90
+ const { buffer } = file;
91
+ if (buffer instanceof ArrayBuffer) {
92
+ return new Uint8Array(buffer, file.byteOffset, file.byteLength);
93
+ }
94
+ // Ensure export uses transferable ArrayBuffer backing.
95
+ return new Uint8Array(file);
96
+ },
97
+ deleteDatabase: () => {
98
+ const env_2 = { stack: [], error: void 0, hasError: false };
99
+ try {
100
+ const deleteDisposer = __addDisposableResource(env_2, new DisposableStack(), false);
101
+ for (const filename of filenamesToDelete) {
102
+ deleteDisposer.defer(() => {
103
+ rmSync(filename, { force: true });
104
+ });
105
+ }
106
+ deleteDisposer.use(disposables);
107
+ }
108
+ catch (e_2) {
109
+ env_2.error = e_2;
110
+ env_2.hasError = true;
111
+ }
112
+ finally {
113
+ __disposeResources(env_2);
114
+ }
115
+ },
116
+ [Symbol.dispose]: () => {
117
+ disposables.dispose();
118
+ },
119
+ });
120
+ }
121
+ catch (e_1) {
122
+ env_1.error = e_1;
123
+ env_1.hasError = true;
124
+ }
125
+ finally {
126
+ __disposeResources(env_1);
127
+ }
38
128
  };
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import { type CreateRun, type RunDeps } from "@evolu/common";
6
+ import { type CreateRun, type RunDefaultDeps } from "@evolu/common";
7
7
  /**
8
8
  * A promise that resolves when a termination signal is received.
9
9
  *
@@ -32,14 +32,14 @@ export interface ShutdownDep {
32
32
  * const deps = { ...createRelayDeps(), console };
33
33
  *
34
34
  * await using run = createRun(deps);
35
- * await using stack = new AsyncDisposableStack();
35
+ * await using disposer = new AsyncDisposableStack();
36
36
  *
37
- * stack.use(await run.orThrow(startRelay({ port: 4000 })));
37
+ * disposer.use(await run.orThrow(startRelay({ port: 4000 })));
38
38
  *
39
39
  * await run.deps.shutdown;
40
40
  * ```
41
41
  *
42
42
  * @group Node.js Run
43
43
  */
44
- export declare const createRun: CreateRun<RunDeps & ShutdownDep>;
44
+ export declare const createRun: CreateRun<RunDefaultDeps & ShutdownDep>;
45
45
  //# sourceMappingURL=Task.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Task.d.ts","sourceRoot":"","sources":["../../src/Task.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,OAAO,EACb,MAAM,eAAe,CAAC;AAEvB;;;;;;;;GAQG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAErC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,SAAS,EAAE,SAAS,CAAC,OAAO,GAAG,WAAW,CAuCtD,CAAC"}
1
+ {"version":3,"file":"Task.d.ts","sourceRoot":"","sources":["../../src/Task.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,cAAc,EACpB,MAAM,eAAe,CAAC;AAEvB;;;;;;;;GAQG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAErC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,SAAS,EAAE,SAAS,CAAC,cAAc,GAAG,WAAW,CAuC7D,CAAC"}
package/dist/src/Task.js CHANGED
@@ -19,9 +19,9 @@ import { createRun as createCommonRun, createUnknownError, } from "@evolu/common
19
19
  * const deps = { ...createRelayDeps(), console };
20
20
  *
21
21
  * await using run = createRun(deps);
22
- * await using stack = new AsyncDisposableStack();
22
+ * await using disposer = new AsyncDisposableStack();
23
23
  *
24
- * stack.use(await run.orThrow(startRelay({ port: 4000 })));
24
+ * disposer.use(await run.orThrow(startRelay({ port: 4000 })));
25
25
  *
26
26
  * await run.deps.shutdown;
27
27
  * ```
@@ -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,sBAoCpC,CAAC"}
@@ -1,4 +1,97 @@
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 { assertNotDisposed } 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
+ disposer.defer(() => {
61
+ nativeBroadcastChannel.onmessage = null;
62
+ nativeBroadcastChannel.close();
63
+ });
64
+ const disposables = disposer.move();
65
+ let onMessageHandler = null;
66
+ return {
67
+ postMessage: (message) => {
68
+ assertNotDisposed(disposables);
69
+ nativeBroadcastChannel.postMessage(message);
70
+ },
71
+ get onMessage() {
72
+ return disposables.disposed ? null : onMessageHandler;
73
+ },
74
+ set onMessage(fn) {
75
+ if (disposables.disposed)
76
+ return;
77
+ onMessageHandler = fn;
78
+ nativeBroadcastChannel.onmessage = fn
79
+ ? (event) => {
80
+ fn(event.data);
81
+ }
82
+ : null;
83
+ },
84
+ [Symbol.dispose]: () => disposables.dispose(),
85
+ };
86
+ }
87
+ catch (e_1) {
88
+ env_1.error = e_1;
89
+ env_1.hasError = true;
90
+ }
91
+ finally {
92
+ __disposeResources(env_1);
93
+ }
94
+ };
2
95
  // TODO: Implement Node.js Worker API
3
96
  //
4
97
  // This module should provide Node.js implementations of the common Worker API:
@@ -3,4 +3,5 @@ export * from "./Crypto.js";
3
3
  export * from "./local-first/Relay.js";
4
4
  export * from "./Task.js";
5
5
  export * from "./WebSocket.js";
6
+ export * from "./Worker.js";
6
7
  //# 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,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
package/dist/src/index.js CHANGED
@@ -3,3 +3,4 @@ export * from "./Crypto.js";
3
3
  export * from "./local-first/Relay.js";
4
4
  export * from "./Task.js";
5
5
  export * from "./WebSocket.js";
6
+ export * from "./Worker.js";
@@ -30,9 +30,9 @@ export declare const createRelayDeps: () => RelayDeps;
30
30
  * const deps = { ...createRelayDeps(), console };
31
31
  *
32
32
  * await using run = createRun(deps);
33
- * await using stack = new AsyncDisposableStack();
33
+ * await using disposer = new AsyncDisposableStack();
34
34
  *
35
- * stack.use(
35
+ * disposer.use(
36
36
  * await run.orThrow(
37
37
  * startRelay({
38
38
  * port: 4000,
@@ -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,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,CA8MlD,CAAC"}
@@ -87,9 +87,9 @@ export const createRelayDeps = () => ({
87
87
  * const deps = { ...createRelayDeps(), console };
88
88
  *
89
89
  * await using run = createRun(deps);
90
- * await using stack = new AsyncDisposableStack();
90
+ * await using disposer = new AsyncDisposableStack();
91
91
  *
92
- * stack.use(
92
+ * disposer.use(
93
93
  * await run.orThrow(
94
94
  * startRelay({
95
95
  * port: 4000,
@@ -111,23 +111,23 @@ export const createRelayDeps = () => ({
111
111
  export const startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isOwnerAllowed, isOwnerWithinQuota, }) => async (run) => {
112
112
  const env_1 = { stack: [], error: void 0, hasError: false };
113
113
  try {
114
- const stack = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
114
+ const disposer = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
115
115
  const console = run.deps.console.child("relay");
116
- stack.defer(() => {
116
+ disposer.defer(() => {
117
117
  console.info("Shutdown complete");
118
118
  });
119
119
  const dbFileExists = existsSync(`${name}.db`);
120
- const sqlite = stack.use(await run.orThrow(createSqlite(name)));
120
+ const sqlite = disposer.use(await run.orThrow(createSqlite(name)));
121
121
  const deps = { ...run.deps, sqlite };
122
122
  if (!dbFileExists) {
123
123
  createBaseSqliteStorageTables(deps);
124
124
  createRelayStorageTables(deps);
125
125
  }
126
- const server = stack.use(createServer());
126
+ const server = disposer.use(createServer());
127
127
  server.once("close", () => {
128
128
  console.info("HTTP server closed");
129
129
  });
130
- const wss = stack.adopt(new WebSocketServer({
130
+ const wss = disposer.adopt(new WebSocketServer({
131
131
  maxPayload: defaultProtocolMessageMaxSize,
132
132
  noServer: true,
133
133
  }), (wss) => new Promise((resolve) => {
@@ -137,7 +137,8 @@ export const startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isO
137
137
  });
138
138
  }));
139
139
  const ownerSocketRelation = createRelation();
140
- const relayRun = stack.use(run.create().addDeps({
140
+ const relayRun = disposer.use(run.create({
141
+ ...run.deps,
141
142
  storage: createRelaySqliteStorage(deps)({
142
143
  isOwnerWithinQuota,
143
144
  }),
@@ -234,7 +235,7 @@ export const startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isO
234
235
  console.debug("ws close", wss.clients.size);
235
236
  });
236
237
  });
237
- stack.defer(() => {
238
+ disposer.defer(() => {
238
239
  console.info("Shutting down...");
239
240
  for (const client of wss.clients) {
240
241
  if (client.readyState === WebSocket.OPEN) {
@@ -246,11 +247,11 @@ export const startRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), isO
246
247
  await once(server, "listening");
247
248
  const address = server.address();
248
249
  assert(address && typeof address !== "string", "Expected TCP address");
249
- const moved = stack.move();
250
+ const disposables = disposer.move();
250
251
  console.info(`Started on port ${address.port}`);
251
252
  return ok({
252
253
  port: address.port,
253
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
254
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
254
255
  });
255
256
  }
256
257
  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-next.3",
4
4
  "description": "Evolu for Node.js",
5
5
  "author": "Daniel Steigerwald <daniel@steigerwald.cz>",
6
6
  "license": "MIT",
@@ -35,7 +35,7 @@
35
35
  "@types/node": "^24.10.9",
36
36
  "@types/ws": "^8.18.1",
37
37
  "typescript": "^6",
38
- "@evolu/common": "8.0.0-next.2",
38
+ "@evolu/common": "8.0.0-next.5",
39
39
  "@evolu/typescript-config": "0.0.2"
40
40
  },
41
41
  "peerDependencies": {
package/src/Sqlite.ts CHANGED
@@ -3,20 +3,29 @@ import {
3
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.
@@ -25,7 +34,9 @@ export const createBetterSqliteDriver: CreateSqliteDriver =
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
  };
package/src/Task.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  createUnknownError,
10
10
  type CreateRun,
11
11
  type Run,
12
- type RunDeps,
12
+ type RunDefaultDeps,
13
13
  } from "@evolu/common";
14
14
 
15
15
  /**
@@ -42,18 +42,18 @@ export interface ShutdownDep {
42
42
  * const deps = { ...createRelayDeps(), console };
43
43
  *
44
44
  * await using run = createRun(deps);
45
- * await using stack = new AsyncDisposableStack();
45
+ * await using disposer = new AsyncDisposableStack();
46
46
  *
47
- * stack.use(await run.orThrow(startRelay({ port: 4000 })));
47
+ * disposer.use(await run.orThrow(startRelay({ port: 4000 })));
48
48
  *
49
49
  * await run.deps.shutdown;
50
50
  * ```
51
51
  *
52
52
  * @group Node.js Run
53
53
  */
54
- export const createRun: CreateRun<RunDeps & ShutdownDep> = <D>(
54
+ export const createRun: CreateRun<RunDefaultDeps & ShutdownDep> = <D>(
55
55
  deps?: D,
56
- ): Run<RunDeps & ShutdownDep & D> => {
56
+ ): Run<RunDefaultDeps & ShutdownDep & D> => {
57
57
  const { promise: shutdown, resolve: resolveShutdown } =
58
58
  Promise.withResolvers<void>();
59
59
 
package/src/Worker.ts CHANGED
@@ -1,3 +1,45 @@
1
+ import type { BroadcastChannel, CreateBroadcastChannel } from "@evolu/common";
2
+ import { assertNotDisposed } 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
+
14
+ disposer.defer(() => {
15
+ nativeBroadcastChannel.onmessage = null;
16
+ nativeBroadcastChannel.close();
17
+ });
18
+
19
+ const disposables = disposer.move();
20
+ let onMessageHandler: ((message: Output) => void) | null = null;
21
+
22
+ return {
23
+ postMessage: (message) => {
24
+ assertNotDisposed(disposables);
25
+ nativeBroadcastChannel.postMessage(message);
26
+ },
27
+ get onMessage() {
28
+ return disposables.disposed ? null : onMessageHandler;
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
+ };
42
+
1
43
  // TODO: Implement Node.js Worker API
2
44
  //
3
45
  // This module should provide Node.js implementations of the common Worker API:
package/src/index.ts CHANGED
@@ -3,3 +3,4 @@ export * from "./Crypto.js";
3
3
  export * from "./local-first/Relay.js";
4
4
  export * from "./Task.js";
5
5
  export * from "./WebSocket.js";
6
+ export * from "./Worker.js";
@@ -69,9 +69,9 @@ export const createRelayDeps = (): RelayDeps => ({
69
69
  * const deps = { ...createRelayDeps(), console };
70
70
  *
71
71
  * await using run = createRun(deps);
72
- * await using stack = new AsyncDisposableStack();
72
+ * await using disposer = new AsyncDisposableStack();
73
73
  *
74
- * stack.use(
74
+ * disposer.use(
75
75
  * await run.orThrow(
76
76
  * startRelay({
77
77
  * port: 4000,
@@ -98,15 +98,15 @@ export const startRelay =
98
98
  isOwnerWithinQuota,
99
99
  }: NodeJsRelayConfig): Task<Relay, never, RelayDeps> =>
100
100
  async (run) => {
101
- await using stack = new AsyncDisposableStack();
101
+ await using disposer = new AsyncDisposableStack();
102
102
  const console = run.deps.console.child("relay");
103
103
 
104
- stack.defer(() => {
104
+ disposer.defer(() => {
105
105
  console.info("Shutdown complete");
106
106
  });
107
107
 
108
108
  const dbFileExists = existsSync(`${name}.db`);
109
- const sqlite = stack.use(await run.orThrow(createSqlite(name)));
109
+ const sqlite = disposer.use(await run.orThrow(createSqlite(name)));
110
110
  const deps = { ...run.deps, sqlite };
111
111
 
112
112
  if (!dbFileExists) {
@@ -114,12 +114,12 @@ export const startRelay =
114
114
  createRelayStorageTables(deps);
115
115
  }
116
116
 
117
- const server = stack.use(createServer());
117
+ const server = disposer.use(createServer());
118
118
  server.once("close", () => {
119
119
  console.info("HTTP server closed");
120
120
  });
121
121
 
122
- const wss = stack.adopt(
122
+ const wss = disposer.adopt(
123
123
  new WebSocketServer({
124
124
  maxPayload: defaultProtocolMessageMaxSize,
125
125
  noServer: true,
@@ -135,8 +135,9 @@ export const startRelay =
135
135
 
136
136
  const ownerSocketRelation = createRelation<OwnerId, WebSocket>();
137
137
 
138
- const relayRun = stack.use(
139
- run.create().addDeps({
138
+ const relayRun = disposer.use(
139
+ run.create({
140
+ ...run.deps,
140
141
  storage: createRelaySqliteStorage(deps)({
141
142
  isOwnerWithinQuota,
142
143
  }),
@@ -278,7 +279,7 @@ export const startRelay =
278
279
  });
279
280
  });
280
281
 
281
- stack.defer(() => {
282
+ disposer.defer(() => {
282
283
  console.info("Shutting down...");
283
284
  for (const client of wss.clients) {
284
285
  if (client.readyState === WebSocket.OPEN) {
@@ -293,13 +294,13 @@ export const startRelay =
293
294
  const address = server.address();
294
295
  assert(address && typeof address !== "string", "Expected TCP address");
295
296
 
296
- const moved = stack.move();
297
+ const disposables = disposer.move();
297
298
 
298
299
  console.info(`Started on port ${address.port}`);
299
300
 
300
301
  return ok({
301
302
  port: address.port,
302
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
303
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
303
304
  });
304
305
  };
305
306