Skip to content

Commit 53335c7

Browse files
authored
Merge pull request #532 from powersync-ja/node-commonjs
Compile node package as CommonJS module
2 parents 2272ad8 + 5054658 commit 53335c7

16 files changed

+2542
-2494
lines changed

.changeset/clean-roses-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/node': patch
3+
---
4+
5+
Include CommonJS distribution for this package.

demos/example-node/src/main.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import repl_factory from 'node:repl';
22
import { once } from 'node:events';
33

44
import { PowerSyncDatabase, SyncStreamConnectionMethod } from '@powersync/node';
5-
import Logger from 'js-logger';
5+
import { default as Logger } from 'js-logger';
66
import { AppSchema, DemoConnector } from './powersync.js';
77
import { exit } from 'node:process';
88

99
const main = async () => {
10-
Logger.useDefaults({ defaultLevel: Logger.WARN });
10+
const logger = Logger.get('PowerSyncDemo');
11+
Logger.useDefaults({ defaultLevel: logger.WARN });
1112

1213
if (!('BACKEND' in process.env) || !('SYNC_SERVICE' in process.env)) {
1314
console.warn(
@@ -21,7 +22,7 @@ const main = async () => {
2122
database: {
2223
dbFilename: 'test.db'
2324
},
24-
logger: Logger
25+
logger
2526
});
2627
console.log(await db.get('SELECT powersync_rs_version();'));
2728

demos/example-node/src/powersync.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export class DemoConnector implements PowerSyncBackendConnector {
3333

3434
const response = await fetch(`${process.env.BACKEND}/api/data/`, {
3535
method: 'POST',
36-
headers: {'Content-Type': 'application/json'},
37-
body: JSON.stringify({batch: entries}),
36+
headers: { 'Content-Type': 'application/json' },
37+
body: JSON.stringify({ batch: entries })
3838
});
3939
if (response.status !== 200) {
4040
throw new Error(`Server returned HTTP ${response.status}: ${await response.text()}`);

demos/example-node/tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"baseUrl": ".",
55
"rootDir": "src",
66
"outDir": "lib",
7-
"strictNullChecks": true
7+
"strictNullChecks": true,
8+
"moduleResolution": "nodenext",
9+
"module": "NodeNext"
810
},
911
"references": [
1012
{

packages/node/package.json

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,32 @@
66
"access": "public"
77
},
88
"description": "PowerSync Node.js SDK. Sync Postgres, MongoDB or MySQL with SQLite in your Node.js app",
9-
"main": "./lib/index.js",
10-
"module": "./lib/index.js",
11-
"types": "./lib/index.d.ts",
129
"files": [
1310
"lib",
1411
"dist",
1512
"download_core.js"
1613
],
1714
"scripts": {
1815
"install": "node download_core.js",
19-
"build": "tsc -b",
16+
"build": "tsc -b && rollup --config",
2017
"build:prod": "tsc -b --sourceMap false",
2118
"clean": "rm -rf lib dist tsconfig.tsbuildinfo dist",
2219
"watch": "tsc -b -w",
2320
"test": "vitest"
2421
},
2522
"type": "module",
23+
"exports": {
24+
".": {
25+
"import": "./lib/index.js",
26+
"require": "./dist/bundle.cjs",
27+
"types": "./lib/index.d.ts"
28+
},
29+
"./worker.js": {
30+
"import": "./lib/worker.js",
31+
"require": "./dist/worker.cjs",
32+
"types": "./lib/worker.d.ts"
33+
}
34+
},
2635
"repository": {
2736
"type": "git",
2837
"url": "git+https://github.com/powersync-ja/powersync-js.git"
@@ -34,17 +43,18 @@
3443
},
3544
"homepage": "https://docs.powersync.com/",
3645
"peerDependencies": {
37-
"@powersync/common": "workspace:^1.22.0",
38-
"@powersync/better-sqlite3": "^0.1.0"
46+
"@powersync/common": "workspace:^1.22.0"
3947
},
4048
"dependencies": {
49+
"@powersync/better-sqlite3": "^0.1.0",
4150
"@powersync/common": "workspace:*",
4251
"async-lock": "^1.4.0",
4352
"bson": "^6.6.0",
4453
"comlink": "^4.4.2"
4554
},
4655
"devDependencies": {
4756
"@types/async-lock": "^1.4.0",
57+
"rollup": "4.14.3",
4858
"typescript": "^5.5.3",
4959
"vitest": "^3.0.5"
5060
},

packages/node/rollup.config.mjs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const plugin = () => {
2+
return {
3+
name: 'mark-as-commonjs',
4+
resolveImportMeta: (property) => {
5+
if (property == 'isBundlingToCommonJs') {
6+
return 'true';
7+
}
8+
9+
return null;
10+
},
11+
};
12+
};
13+
14+
export default [
15+
{
16+
input: 'lib/index.js',
17+
plugins: [plugin()],
18+
output: {
19+
file: 'dist/bundle.cjs',
20+
format: 'cjs',
21+
sourcemap: true
22+
}
23+
},
24+
{
25+
input: 'lib/db/DefaultWorker.js',
26+
plugins: [plugin()],
27+
output: {
28+
file: 'dist/DefaultWorker.cjs',
29+
format: 'cjs',
30+
sourcemap: true
31+
}
32+
},
33+
{
34+
input: 'lib/worker.js',
35+
plugins: [plugin()],
36+
output: {
37+
file: 'dist/worker.cjs',
38+
format: 'cjs',
39+
sourcemap: true
40+
}
41+
}
42+
];

packages/node/src/db/BetterSQLite3DBAdapter.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import {
1010
LockContext,
1111
Transaction,
1212
DBLockOptions,
13-
QueryResult,
14-
SQLOpenOptions
13+
QueryResult
1514
} from '@powersync/common';
1615
import { Remote } from 'comlink';
1716
import { AsyncResource } from 'node:async_hooks';
1817
import { AsyncDatabase, AsyncDatabaseOpener } from './AsyncDatabase.js';
1918
import { RemoteConnection } from './RemoteConnection.js';
19+
import { NodeSQLOpenOptions } from './options.js';
2020

2121
export type BetterSQLite3LockContext = LockContext & {
2222
executeBatch(query: string, params?: any[][]): Promise<QueryResult>;
@@ -30,7 +30,7 @@ const READ_CONNECTIONS = 5;
3030
* Adapter for better-sqlite3
3131
*/
3232
export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> implements DBAdapter {
33-
private readonly options: SQLOpenOptions;
33+
private readonly options: NodeSQLOpenOptions;
3434
public readonly name: string;
3535

3636
private readConnections: RemoteConnection[];
@@ -39,9 +39,13 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
3939
private readonly readQueue: Array<(connection: RemoteConnection) => void> = [];
4040
private readonly writeQueue: Array<() => void> = [];
4141

42-
constructor(options: SQLOpenOptions) {
42+
constructor(options: NodeSQLOpenOptions) {
4343
super();
4444

45+
if (options.readWorkerCount != null && options.readWorkerCount < 1) {
46+
throw `Needs at least one worker for reads, got ${options.readWorkerCount}`;
47+
}
48+
4549
this.options = options;
4650
this.name = options.dbFilename;
4751
}
@@ -53,7 +57,17 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
5357
}
5458

5559
const openWorker = async (isWriter: boolean) => {
56-
const worker = new Worker(new URL('./SqliteWorker.js', import.meta.url), {name: isWriter ? `write ${dbFilePath}` : `read ${dbFilePath}`});
60+
const isCommonJsModule = import.meta.isBundlingToCommonJs ?? false;
61+
let worker: Worker;
62+
const workerName = isWriter ? `write ${dbFilePath}` : `read ${dbFilePath}`;
63+
64+
const workerFactory = this.options.openWorker ?? ((...args) => new Worker(...args));
65+
if (isCommonJsModule) {
66+
worker = workerFactory(path.resolve(__dirname, 'DefaultWorker.cjs'), { name: workerName });
67+
} else {
68+
worker = workerFactory(new URL('./DefaultWorker.js', import.meta.url), { name: workerName});
69+
}
70+
5771
const listeners = new WeakMap<EventListenerOrEventListenerObject, (e: any) => void>();
5872

5973
const comlink = Comlink.wrap<AsyncDatabaseOpener>({
@@ -94,7 +108,8 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
94108
// Open the writer first to avoid multiple threads enabling WAL concurrently (causing "database is locked" errors).
95109
this.writeConnection = await openWorker(true);
96110
const createWorkers: Promise<RemoteConnection>[] = [];
97-
for (let i = 0; i < READ_CONNECTIONS; i++) {
111+
const amountOfReaders = this.options.readWorkerCount ?? READ_CONNECTIONS;
112+
for (let i = 0; i < amountOfReaders; i++) {
98113
createWorkers.push(openWorker(false));
99114
}
100115
this.readConnections = await Promise.all(createWorkers);

packages/node/src/db/DefaultWorker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { startPowerSyncWorker } from './SqliteWorker.js';
2+
3+
startPowerSyncWorker();

packages/node/src/db/PowerSyncDatabase.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ import {
44
BucketStorageAdapter,
55
DBAdapter,
66
PowerSyncBackendConnector,
7+
PowerSyncDatabaseOptions,
78
PowerSyncDatabaseOptionsWithSettings,
8-
SqliteBucketStorage
9+
SqliteBucketStorage,
10+
SQLOpenFactory
911
} from '@powersync/common';
1012

1113
import { NodeRemote } from '../sync/stream/NodeRemote.js';
1214
import { NodeStreamingSyncImplementation } from '../sync/stream/NodeStreamingSyncImplementation.js';
1315

1416
import { BetterSQLite3DBAdapter } from './BetterSQLite3DBAdapter.js';
17+
import { NodeSQLOpenOptions } from './options.js';
18+
19+
export type NodePowerSyncDatabaseOptions = PowerSyncDatabaseOptions & {
20+
database: DBAdapter | SQLOpenFactory | NodeSQLOpenOptions;
21+
};
1522

1623
/**
1724
* A PowerSync database which provides SQLite functionality
@@ -28,6 +35,10 @@ import { BetterSQLite3DBAdapter } from './BetterSQLite3DBAdapter.js';
2835
* ```
2936
*/
3037
export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
38+
constructor(options: NodePowerSyncDatabaseOptions) {
39+
super(options);
40+
}
41+
3142
async _initialize(): Promise<void> {
3243
await (this.database as BetterSQLite3DBAdapter).initialize();
3344
}

packages/node/src/db/SqliteWorker.ts

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as path from 'node:path';
12
import BetterSQLite3Database, { Database } from '@powersync/better-sqlite3';
23
import * as Comlink from 'comlink';
34
import { parentPort, threadId } from 'node:worker_threads';
@@ -81,10 +82,16 @@ class BlockingAsyncDatabase implements AsyncDatabase {
8182
}
8283

8384
class BetterSqliteWorker implements AsyncDatabaseOpener {
85+
options: PowerSyncWorkerOptions;
86+
87+
constructor(options: PowerSyncWorkerOptions) {
88+
this.options = options;
89+
}
90+
8491
async open(path: string, isWriter: boolean): Promise<AsyncDatabase> {
8592
const baseDB = new BetterSQLite3Database(path);
8693
baseDB.pragma('journal_mode = WAL');
87-
loadExtension(baseDB);
94+
baseDB.loadExtension(this.options.extensionPath(), 'sqlite3_powersync_init');
8895
if (!isWriter) {
8996
baseDB.pragma('query_only = true');
9097
}
@@ -96,21 +103,43 @@ class BetterSqliteWorker implements AsyncDatabaseOpener {
96103
}
97104
}
98105

99-
const loadExtension = (db: Database) => {
100-
const platform = OS.platform();
101-
let extensionPath: string;
102-
if (platform === 'win32') {
103-
extensionPath = 'powersync.dll';
104-
} else if (platform === 'linux') {
105-
extensionPath = 'libpowersync.so';
106-
} else if (platform === 'darwin') {
107-
extensionPath = 'libpowersync.dylib';
108-
} else {
109-
throw 'Unknown platform, PowerSync for Node.js currently supports Windows, Linux and macOS.';
110-
}
106+
export interface PowerSyncWorkerOptions {
107+
/**
108+
* A function responsible for finding the powersync DLL/so/dylib file.
109+
*
110+
* @returns The absolute path of the PowerSync SQLite core extensions library.
111+
*/
112+
extensionPath: () => string;
113+
}
111114

112-
const resolved = url.fileURLToPath(new URL(`../${extensionPath}`, import.meta.url));
113-
db.loadExtension(resolved, 'sqlite3_powersync_init');
114-
};
115+
export function startPowerSyncWorker(options?: Partial<PowerSyncWorkerOptions>) {
116+
const resolvedOptions: PowerSyncWorkerOptions = {
117+
extensionPath() {
118+
const isCommonJsModule = import.meta.isBundlingToCommonJs ?? false;
119+
120+
const platform = OS.platform();
121+
let extensionPath: string;
122+
if (platform === 'win32') {
123+
extensionPath = 'powersync.dll';
124+
} else if (platform === 'linux') {
125+
extensionPath = 'libpowersync.so';
126+
} else if (platform === 'darwin') {
127+
extensionPath = 'libpowersync.dylib';
128+
} else {
129+
throw 'Unknown platform, PowerSync for Node.js currently supports Windows, Linux and macOS.';
130+
}
131+
132+
let resolved: string;
133+
if (isCommonJsModule) {
134+
resolved = path.resolve(__dirname, '../lib/', extensionPath);
135+
} else {
136+
resolved = url.fileURLToPath(new URL(`../${extensionPath}`, import.meta.url));
137+
}
115138

116-
Comlink.expose(new BetterSqliteWorker(), parentPort! as Comlink.Endpoint);
139+
return resolved;
140+
},
141+
...options,
142+
};
143+
144+
Comlink.expose(new BetterSqliteWorker(resolvedOptions), parentPort! as Comlink.Endpoint);
145+
}

packages/node/src/db/options.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { type Worker } from 'node:worker_threads';
2+
import { SQLOpenOptions } from '@powersync/common';
3+
4+
export type WorkerOpener = (...args: ConstructorParameters<typeof Worker>) => InstanceType<typeof Worker>;
5+
6+
/**
7+
* The {@link SQLOpenOptions} available across all PowerSync SDKs for JavaScript extended with
8+
* Node.JS-specific options.
9+
*/
10+
export interface NodeSQLOpenOptions extends SQLOpenOptions {
11+
/**
12+
* The Node.JS SDK will use one worker to run writing queries and additional workers to run reads.
13+
* This option controls how many workers to use for reads.
14+
*/
15+
readWorkerCount?: number;
16+
/**
17+
* A callback to allow customizing how the Node.JS SDK loads workers. This can be customized to
18+
* use workers at different paths.
19+
*
20+
* @param args The arguments that would otherwise be passed to the {@link Worker} constructor.
21+
* @returns the resolved worker.
22+
*/
23+
openWorker?: WorkerOpener;
24+
}

packages/node/src/node.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
interface ImportMeta {
2+
isBundlingToCommonJs?: boolean; // This property is set by our rollup configuration
3+
}

packages/node/src/worker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './db/SqliteWorker.js';

0 commit comments

Comments
 (0)