Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clean-roses-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/node': patch
---

Include CommonJS distribution for this package.
7 changes: 4 additions & 3 deletions demos/example-node/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import repl_factory from 'node:repl';
import { once } from 'node:events';

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

const main = async () => {
Logger.useDefaults({ defaultLevel: Logger.WARN });
const logger = Logger.get('PowerSyncDemo');
Logger.useDefaults({ defaultLevel: logger.WARN });

if (!('BACKEND' in process.env) || !('SYNC_SERVICE' in process.env)) {
console.warn(
Expand All @@ -21,7 +22,7 @@ const main = async () => {
database: {
dbFilename: 'test.db'
},
logger: Logger
logger
});
console.log(await db.get('SELECT powersync_rs_version();'));

Expand Down
4 changes: 2 additions & 2 deletions demos/example-node/src/powersync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export class DemoConnector implements PowerSyncBackendConnector {

const response = await fetch(`${process.env.BACKEND}/api/data/`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({batch: entries}),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ batch: entries })
});
if (response.status !== 200) {
throw new Error(`Server returned HTTP ${response.status}: ${await response.text()}`);
Expand Down
4 changes: 3 additions & 1 deletion demos/example-node/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"baseUrl": ".",
"rootDir": "src",
"outDir": "lib",
"strictNullChecks": true
"strictNullChecks": true,
"moduleResolution": "nodenext",
"module": "NodeNext"
},
"references": [
{
Expand Down
22 changes: 16 additions & 6 deletions packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,32 @@
"access": "public"
},
"description": "PowerSync Node.js SDK. Sync Postgres, MongoDB or MySQL with SQLite in your Node.js app",
"main": "./lib/index.js",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"files": [
"lib",
"dist",
"download_core.js"
],
"scripts": {
"install": "node download_core.js",
"build": "tsc -b",
"build": "tsc -b && rollup --config",
"build:prod": "tsc -b --sourceMap false",
"clean": "rm -rf lib dist tsconfig.tsbuildinfo dist",
"watch": "tsc -b -w",
"test": "vitest"
},
"type": "module",
"exports": {
".": {
"import": "./lib/index.js",
"require": "./dist/bundle.cjs",
"types": "./lib/index.d.ts"
},
"./worker.js": {
"import": "./lib/worker.js",
"require": "./dist/worker.cjs",
"types": "./lib/worker.d.ts"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/powersync-ja/powersync-js.git"
Expand All @@ -34,17 +43,18 @@
},
"homepage": "https://docs.powersync.com/",
"peerDependencies": {
"@powersync/common": "workspace:^1.22.0",
"@powersync/better-sqlite3": "^0.1.0"
"@powersync/common": "workspace:^1.22.0"
},
"dependencies": {
"@powersync/better-sqlite3": "^0.1.0",
"@powersync/common": "workspace:*",
"async-lock": "^1.4.0",
"bson": "^6.6.0",
"comlink": "^4.4.2"
},
"devDependencies": {
"@types/async-lock": "^1.4.0",
"rollup": "4.14.3",
"typescript": "^5.5.3",
"vitest": "^3.0.5"
},
Expand Down
42 changes: 42 additions & 0 deletions packages/node/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const plugin = () => {
return {
name: 'mark-as-commonjs',
resolveImportMeta: (property) => {
if (property == 'isBundlingToCommonJs') {
return 'true';
}

return null;
},
};
};

export default [
{
input: 'lib/index.js',
plugins: [plugin()],
output: {
file: 'dist/bundle.cjs',
format: 'cjs',
sourcemap: true
}
},
{
input: 'lib/db/DefaultWorker.js',
plugins: [plugin()],
output: {
file: 'dist/DefaultWorker.cjs',
format: 'cjs',
sourcemap: true
}
},
{
input: 'lib/worker.js',
plugins: [plugin()],
output: {
file: 'dist/worker.cjs',
format: 'cjs',
sourcemap: true
}
}
];
27 changes: 21 additions & 6 deletions packages/node/src/db/BetterSQLite3DBAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import {
LockContext,
Transaction,
DBLockOptions,
QueryResult,
SQLOpenOptions
QueryResult
} from '@powersync/common';
import { Remote } from 'comlink';
import { AsyncResource } from 'node:async_hooks';
import { AsyncDatabase, AsyncDatabaseOpener } from './AsyncDatabase.js';
import { RemoteConnection } from './RemoteConnection.js';
import { NodeSQLOpenOptions } from './options.js';

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

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

constructor(options: SQLOpenOptions) {
constructor(options: NodeSQLOpenOptions) {
super();

if (options.readWorkers != null && options.readWorkers < 1) {
throw `Needs at least one worker for reads, got ${options.readWorkers}`;
}

this.options = options;
this.name = options.dbFilename;
}
Expand All @@ -53,7 +57,17 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
}

const openWorker = async (isWriter: boolean) => {
const worker = new Worker(new URL('./SqliteWorker.js', import.meta.url), {name: isWriter ? `write ${dbFilePath}` : `read ${dbFilePath}`});
const isCommonJsModule = import.meta.isBundlingToCommonJs ?? false;
let worker: Worker;
const workerName = isWriter ? `write ${dbFilePath}` : `read ${dbFilePath}`;

const workerFactory = this.options.openWorker ?? ((...args) => new Worker(...args));
if (isCommonJsModule) {
worker = workerFactory(path.resolve(__dirname, 'DefaultWorker.cjs'), { name: workerName });
} else {
worker = workerFactory(new URL('./DefaultWorker.js', import.meta.url), { name: workerName });
}

const listeners = new WeakMap<EventListenerOrEventListenerObject, (e: any) => void>();

const comlink = Comlink.wrap<AsyncDatabaseOpener>({
Expand Down Expand Up @@ -94,7 +108,8 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
// Open the writer first to avoid multiple threads enabling WAL concurrently (causing "database is locked" errors).
this.writeConnection = await openWorker(true);
const createWorkers: Promise<RemoteConnection>[] = [];
for (let i = 0; i < READ_CONNECTIONS; i++) {
const amountOfReaders = this.options.readWorkers ?? READ_CONNECTIONS;
for (let i = 0; i < amountOfReaders; i++) {
createWorkers.push(openWorker(false));
}
this.readConnections = await Promise.all(createWorkers);
Expand Down
3 changes: 3 additions & 0 deletions packages/node/src/db/DefaultWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { startPowerSyncWorker } from './SqliteWorker.js';

startPowerSyncWorker();
13 changes: 12 additions & 1 deletion packages/node/src/db/PowerSyncDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import {
BucketStorageAdapter,
DBAdapter,
PowerSyncBackendConnector,
PowerSyncDatabaseOptions,
PowerSyncDatabaseOptionsWithSettings,
SqliteBucketStorage
SqliteBucketStorage,
SQLOpenFactory
} from '@powersync/common';

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

import { BetterSQLite3DBAdapter } from './BetterSQLite3DBAdapter.js';
import { NodeSQLOpenOptions } from './options.js';

export type NodePowerSyncDatabaseOptions = PowerSyncDatabaseOptions & {
database: DBAdapter | SQLOpenFactory | NodeSQLOpenOptions;
};

/**
* A PowerSync database which provides SQLite functionality
Expand All @@ -28,6 +35,10 @@ import { BetterSQLite3DBAdapter } from './BetterSQLite3DBAdapter.js';
* ```
*/
export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
constructor(options: NodePowerSyncDatabaseOptions) {
super(options);
}

async _initialize(): Promise<void> {
await (this.database as BetterSQLite3DBAdapter).initialize();
}
Expand Down
63 changes: 46 additions & 17 deletions packages/node/src/db/SqliteWorker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as path from 'node:path';
import BetterSQLite3Database, { Database } from '@powersync/better-sqlite3';
import * as Comlink from 'comlink';
import { parentPort, threadId } from 'node:worker_threads';
Expand Down Expand Up @@ -81,10 +82,16 @@ class BlockingAsyncDatabase implements AsyncDatabase {
}

class BetterSqliteWorker implements AsyncDatabaseOpener {
options: PowerSyncWorkerOptions;

constructor(options: PowerSyncWorkerOptions) {
this.options = options;
}

async open(path: string, isWriter: boolean): Promise<AsyncDatabase> {
const baseDB = new BetterSQLite3Database(path);
baseDB.pragma('journal_mode = WAL');
loadExtension(baseDB);
baseDB.loadExtension(this.options.extensionPath(), 'sqlite3_powersync_init');
if (!isWriter) {
baseDB.pragma('query_only = true');
}
Expand All @@ -96,21 +103,43 @@ class BetterSqliteWorker implements AsyncDatabaseOpener {
}
}

const loadExtension = (db: Database) => {
const platform = OS.platform();
let extensionPath: string;
if (platform === 'win32') {
extensionPath = 'powersync.dll';
} else if (platform === 'linux') {
extensionPath = 'libpowersync.so';
} else if (platform === 'darwin') {
extensionPath = 'libpowersync.dylib';
} else {
throw 'Unknown platform, PowerSync for Node.js currently supports Windows, Linux and macOS.';
}
export interface PowerSyncWorkerOptions {
/**
* A function responsible for finding the powersync DLL/so/dylib file.
*
* @returns The absolute path of the PowerSync SQLite core extensions library.
*/
extensionPath: () => string;
}

const resolved = url.fileURLToPath(new URL(`../${extensionPath}`, import.meta.url));
db.loadExtension(resolved, 'sqlite3_powersync_init');
};
export function startPowerSyncWorker(options?: Partial<PowerSyncWorkerOptions>) {
const resolvedOptions: PowerSyncWorkerOptions = {
...options,
extensionPath() {
const isCommonJsModule = import.meta.isBundlingToCommonJs ?? false;

const platform = OS.platform();
let extensionPath: string;
if (platform === 'win32') {
extensionPath = 'powersync.dll';
} else if (platform === 'linux') {
extensionPath = 'libpowersync.so';
} else if (platform === 'darwin') {
extensionPath = 'libpowersync.dylib';
} else {
throw 'Unknown platform, PowerSync for Node.js currently supports Windows, Linux and macOS.';
}

Comlink.expose(new BetterSqliteWorker(), parentPort! as Comlink.Endpoint);
let resolved: string;
if (isCommonJsModule) {
resolved = path.resolve(__dirname, '../lib/', extensionPath);
} else {
resolved = url.fileURLToPath(new URL(`../${extensionPath}`, import.meta.url));
}

return resolved;
}
};

Comlink.expose(new BetterSqliteWorker(resolvedOptions), parentPort! as Comlink.Endpoint);
}
22 changes: 22 additions & 0 deletions packages/node/src/db/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type Worker } from 'node:worker_threads';
import { SQLOpenOptions } from '@powersync/common';

/**
* The {@link SQLOpenOptions} available across all PowerSync SDKs for JavaScript extended with
* Node.JS-specific options.
*/
export interface NodeSQLOpenOptions extends SQLOpenOptions {
/**
* The Node.JS SDK will use one worker to run writing queries and additional workers to run reads.
* This option controls how many workers to use for reads.
*/
readWorkers?: number;
/**
* A callback to allow customizing how the Node.JS SDK loads workers. This can be customized to
* use workers at different paths.
*
* @param args The arguments that would otherwise be passed to the {@link Worker} constructor.
* @returns the resolved worker.
*/
openWorker?: (...args: ConstructorParameters<typeof Worker>) => InstanceType<typeof Worker>;
}
3 changes: 3 additions & 0 deletions packages/node/src/node.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface ImportMeta {
isBundlingToCommonJs?: boolean; // This property is set by our rollup configuration
}
1 change: 1 addition & 0 deletions packages/node/src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './db/SqliteWorker.js';
Loading