@powersync/node 0.11.1 → 0.13.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.
Files changed (36) hide show
  1. package/README.md +91 -10
  2. package/dist/DefaultWorker.cjs +140 -62
  3. package/dist/DefaultWorker.cjs.map +1 -1
  4. package/dist/bundle.cjs +31 -13
  5. package/dist/bundle.cjs.map +1 -1
  6. package/dist/bundle.d.cts +23 -1
  7. package/dist/worker.cjs +141 -62
  8. package/dist/worker.cjs.map +1 -1
  9. package/dist/worker.d.cts +9 -1
  10. package/download_core.js +75 -32
  11. package/lib/db/AsyncDatabase.d.ts +7 -2
  12. package/lib/db/BetterSqliteWorker.d.ts +26 -0
  13. package/lib/db/BetterSqliteWorker.js +58 -0
  14. package/lib/db/NodeSqliteWorker.d.ts +21 -0
  15. package/lib/db/NodeSqliteWorker.js +46 -0
  16. package/lib/db/PowerSyncDatabase.js +2 -2
  17. package/lib/db/SqliteWorker.d.ts +8 -0
  18. package/lib/db/SqliteWorker.js +75 -98
  19. package/lib/db/{BetterSQLite3DBAdapter.d.ts → WorkerConnectionPool.d.ts} +2 -2
  20. package/lib/db/{BetterSQLite3DBAdapter.js → WorkerConnectionPool.js} +30 -6
  21. package/lib/db/options.d.ts +23 -1
  22. package/lib/libpowersync_aarch64.dylib +0 -0
  23. package/lib/libpowersync_aarch64.so +0 -0
  24. package/lib/libpowersync_armv7.so +0 -0
  25. package/lib/libpowersync_riscv64gc.so +0 -0
  26. package/lib/libpowersync_x64.dylib +0 -0
  27. package/lib/libpowersync_x86.so +0 -0
  28. package/lib/powersync_aarch64.dll +0 -0
  29. package/lib/powersync_x64.dll +0 -0
  30. package/lib/powersync_x86.dll +0 -0
  31. package/lib/utils/modules.d.ts +2 -0
  32. package/lib/utils/modules.js +5 -0
  33. package/lib/utils/modules_commonjs.d.ts +2 -0
  34. package/lib/utils/modules_commonjs.js +6 -0
  35. package/package.json +15 -8
  36. /package/lib/{libpowersync.so → libpowersync_x64.so} +0 -0
package/download_core.js CHANGED
@@ -1,39 +1,25 @@
1
- // TODO: Make this a pre-publish hook and just bundle everything
2
1
  import { createHash } from 'node:crypto';
3
- import * as OS from 'node:os';
4
2
  import * as fs from 'node:fs/promises';
5
3
  import * as path from 'node:path';
6
4
  import { Readable } from 'node:stream';
7
5
  import { finished } from 'node:stream/promises';
8
- import { exit } from 'node:process';
9
6
 
10
7
  // When changing this version, run node download_core.js update_hashes
11
8
  const version = '0.4.6';
12
9
  const versionHashes = {
13
10
  'powersync_x64.dll': '5efaa9ad4975094912a36843cb7b503376cacd233d21ae0956f0f4b42dcb457b',
11
+ 'powersync_x86.dll': '4151ba8aa6f024b50b7aebe52ba59f2c5be54e3fed26f7f3f48e1127dcda027d',
12
+ 'powersync_aarch64.dll': '3abe46074432593ff5cfc2098b186c592f020c5cfa81285f8e49962732a94bf5',
13
+ 'libpowersync_x86.so': '1321a7de13fda0b2de7d2bc231a68cb5691f84010f3858e5cf02e47f88ba6f4a',
14
14
  'libpowersync_x64.so': 'e9d78620d69d3cf7d57353891fe0bf85b79d326b42c4669b9500b9e610388f76',
15
15
  'libpowersync_aarch64.so': '0d84c0dc0134fc89af65724d11e2c45e3c15569c575ecda52d0ec2fa2aeec495',
16
+ 'libpowersync_armv7.so': 'c7887181ce9c524b68a7ac284ab447b8584511c87527ca26186e5874bf9ba3d6',
17
+ 'libpowersync_riscv64gc.so': 'a89f3a71f22f707707d97517e9310e42e2a57dc5343cee08d09002a8cea048d5',
16
18
  'libpowersync_x64.dylib': '9b484eaf361451f7758ca6ad53190a73563be930a8f8a39ccefd29390046ef6c',
17
19
  'libpowersync_aarch64.dylib': 'bfb4f1ec207b298aff560f1825f8123d24316edaa27b6df3a17dd49466576b92'
18
20
  };
19
21
 
20
- const platform = OS.platform();
21
- let destination;
22
- let asset;
23
-
24
- if (platform === 'win32') {
25
- asset = 'powersync_x64.dll';
26
- destination = 'powersync.dll';
27
- } else if (platform === 'linux') {
28
- asset = OS.arch() === 'x64' ? 'libpowersync_x64.so' : 'libpowersync_aarch64.so';
29
- destination = 'libpowersync.so';
30
- } else if (platform === 'darwin') {
31
- asset = OS.arch() === 'x64' ? 'libpowersync_x64.dylib' : 'libpowersync_aarch64.dylib';
32
- destination = 'libpowersync.dylib';
33
- }
34
-
35
- const expectedHash = versionHashes[asset];
36
- const destinationPath = path.resolve('lib', destination);
22
+ const assets = Object.keys(versionHashes);
37
23
 
38
24
  const hashStream = async (input) => {
39
25
  for await (const chunk of input.pipe(createHash('sha256')).setEncoding('hex')) {
@@ -41,9 +27,9 @@ const hashStream = async (input) => {
41
27
  }
42
28
  };
43
29
 
44
- const hashLocal = async () => {
30
+ const hashLocal = async (filePath) => {
45
31
  try {
46
- const handle = await fs.open(destinationPath, 'r');
32
+ const handle = await fs.open(filePath, 'r');
47
33
  const input = handle.createReadStream();
48
34
 
49
35
  const result = await hashStream(input);
@@ -54,31 +40,88 @@ const hashLocal = async () => {
54
40
  }
55
41
  };
56
42
 
57
- const download = async () => {
58
- if ((await hashLocal()) == expectedHash) {
59
- console.debug('Local copy is up-to-date, skipping download');
60
- exit(0);
43
+ const downloadAsset = async (asset) => {
44
+ const destinationPath = path.resolve('lib', asset);
45
+ const expectedHash = versionHashes[asset];
46
+
47
+ // Check if file exists and has correct hash
48
+ const currentHash = await hashLocal(destinationPath);
49
+ if (currentHash == expectedHash) {
50
+ console.debug(`${asset} is up-to-date, skipping download`);
51
+ return;
61
52
  }
62
53
 
63
54
  const url = `https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v${version}/${asset}`;
55
+ console.log(`Downloading ${url}`);
64
56
  const response = await fetch(url);
65
57
  if (response.status != 200) {
66
58
  throw `Could not download ${url}`;
67
59
  }
68
60
 
61
+ const file = await fs.open(destinationPath, 'w');
62
+ await finished(Readable.fromWeb(response.body).pipe(file.createWriteStream()));
63
+ await file.close();
64
+
65
+ const hashAfterDownloading = await hashLocal(destinationPath);
66
+ if (hashAfterDownloading != expectedHash) {
67
+ throw `Unexpected hash after downloading ${asset} (got ${hashAfterDownloading}, expected ${expectedHash})`;
68
+ }
69
+ console.log(`Successfully downloaded ${asset}`);
70
+ };
71
+
72
+ const checkAsset = async (asset) => {
73
+ const destinationPath = path.resolve('lib', asset);
74
+ const expectedHash = versionHashes[asset];
75
+ const currentHash = await hashLocal(destinationPath);
76
+
77
+ return {
78
+ asset,
79
+ destinationPath,
80
+ expectedHash,
81
+ currentHash,
82
+ exists: currentHash !== null,
83
+ isValid: currentHash == expectedHash
84
+ };
85
+ };
86
+
87
+ const download = async () => {
69
88
  try {
70
89
  await fs.access('lib');
71
90
  } catch {
72
91
  await fs.mkdir('lib');
73
92
  }
74
93
 
75
- const file = await fs.open(destinationPath, 'w');
76
- await finished(Readable.fromWeb(response.body).pipe(file.createWriteStream()));
77
- await file.close();
94
+ // First check all assets
95
+ console.log('Checking existing files...');
96
+ const checks = await Promise.all(assets.map((asset) => checkAsset(asset, asset)));
78
97
 
79
- const hashAfterDownloading = await hashLocal();
80
- if (hashAfterDownloading != expectedHash) {
81
- throw `Unexpected hash after downloading (got ${hashAfterDownloading}, expected ${expectedHash})`;
98
+ const toDownload = checks.filter((check) => !check.isValid);
99
+ const upToDate = checks.filter((check) => check.isValid);
100
+
101
+ // Print summary
102
+ if (upToDate.length > 0) {
103
+ console.log('\nUp-to-date files:');
104
+ for (const check of upToDate) {
105
+ console.log(` ✓ ${check.asset}`);
106
+ }
107
+ }
108
+
109
+ if (toDownload.length > 0) {
110
+ console.log('\nFiles to download:');
111
+ for (const check of toDownload) {
112
+ if (!check.exists) {
113
+ console.log(` • ${check.asset} (missing)`);
114
+ } else {
115
+ console.log(` • ${check.asset} (hash mismatch)`);
116
+ }
117
+ }
118
+
119
+ console.log('\nStarting downloads...');
120
+ await Promise.all(toDownload.map((check) => downloadAsset(check.asset)));
121
+
122
+ console.log('\nAll downloads completed successfully!');
123
+ } else {
124
+ console.log('\nAll files are up-to-date, nothing to download.');
82
125
  }
83
126
  };
84
127
 
@@ -1,17 +1,22 @@
1
1
  import { QueryResult } from '@powersync/common';
2
+ import { NodeDatabaseImplementation } from './options.js';
2
3
  export type ProxiedQueryResult = Omit<QueryResult, 'rows'> & {
3
4
  rows?: {
4
5
  _array: any[];
5
6
  length: number;
6
7
  };
7
8
  };
9
+ export interface AsyncDatabaseOpenOptions {
10
+ path: string;
11
+ isWriter: boolean;
12
+ implementation: NodeDatabaseImplementation;
13
+ }
8
14
  export interface AsyncDatabaseOpener {
9
- open(path: string, isWriter: boolean): Promise<AsyncDatabase>;
15
+ open(options: AsyncDatabaseOpenOptions): Promise<AsyncDatabase>;
10
16
  }
11
17
  export interface AsyncDatabase {
12
18
  execute: (query: string, params: any[]) => Promise<ProxiedQueryResult>;
13
19
  executeRaw: (query: string, params: any[]) => Promise<any[][]>;
14
20
  executeBatch: (query: string, params: any[][]) => Promise<ProxiedQueryResult>;
15
21
  close: () => Promise<void>;
16
- collectCommittedUpdates: () => Promise<string[]>;
17
22
  }
@@ -0,0 +1,26 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ import { AsyncDatabase, AsyncDatabaseOpenOptions } from './AsyncDatabase.js';
3
+ import { PowerSyncWorkerOptions } from './SqliteWorker.js';
4
+ declare class BlockingAsyncDatabase implements AsyncDatabase {
5
+ private readonly db;
6
+ constructor(db: Database);
7
+ close(): Promise<void>;
8
+ execute(query: string, params: any[]): Promise<{
9
+ rowsAffected: number;
10
+ rows: {
11
+ _array: unknown[];
12
+ length: number;
13
+ };
14
+ insertId?: undefined;
15
+ } | {
16
+ rowsAffected: number;
17
+ insertId: number;
18
+ rows?: undefined;
19
+ }>;
20
+ executeRaw(query: string, params: any[]): Promise<any[][]>;
21
+ executeBatch(query: string, params: any[][]): Promise<{
22
+ rowsAffected: number;
23
+ }>;
24
+ }
25
+ export declare function openDatabase(worker: PowerSyncWorkerOptions, options: AsyncDatabaseOpenOptions): Promise<BlockingAsyncDatabase>;
26
+ export {};
@@ -0,0 +1,58 @@
1
+ import { threadId } from 'node:worker_threads';
2
+ class BlockingAsyncDatabase {
3
+ db;
4
+ constructor(db) {
5
+ this.db = db;
6
+ db.function('node_thread_id', () => threadId);
7
+ }
8
+ async close() {
9
+ this.db.close();
10
+ }
11
+ async execute(query, params) {
12
+ const stmt = this.db.prepare(query);
13
+ if (stmt.reader) {
14
+ const rows = stmt.all(params);
15
+ return {
16
+ rowsAffected: 0,
17
+ rows: {
18
+ _array: rows,
19
+ length: rows.length
20
+ }
21
+ };
22
+ }
23
+ else {
24
+ const info = stmt.run(params);
25
+ return {
26
+ rowsAffected: info.changes,
27
+ insertId: Number(info.lastInsertRowid)
28
+ };
29
+ }
30
+ }
31
+ async executeRaw(query, params) {
32
+ const stmt = this.db.prepare(query);
33
+ if (stmt.reader) {
34
+ return stmt.raw().all(params);
35
+ }
36
+ else {
37
+ stmt.raw().run(params);
38
+ return [];
39
+ }
40
+ }
41
+ async executeBatch(query, params) {
42
+ params = params ?? [];
43
+ let rowsAffected = 0;
44
+ const stmt = this.db.prepare(query);
45
+ for (const paramSet of params) {
46
+ const info = stmt.run(paramSet);
47
+ rowsAffected += info.changes;
48
+ }
49
+ return { rowsAffected };
50
+ }
51
+ }
52
+ export async function openDatabase(worker, options) {
53
+ const BetterSQLite3Database = await worker.loadBetterSqlite3();
54
+ const baseDB = new BetterSQLite3Database(options.path);
55
+ baseDB.loadExtension(worker.extensionPath(), 'sqlite3_powersync_init');
56
+ const asyncDb = new BlockingAsyncDatabase(baseDB);
57
+ return asyncDb;
58
+ }
@@ -0,0 +1,21 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import { AsyncDatabase, AsyncDatabaseOpenOptions } from './AsyncDatabase.js';
3
+ import { PowerSyncWorkerOptions } from './SqliteWorker.js';
4
+ declare class BlockingNodeDatabase implements AsyncDatabase {
5
+ private readonly db;
6
+ constructor(db: DatabaseSync, write: boolean);
7
+ close(): Promise<void>;
8
+ execute(query: string, params: any[]): Promise<{
9
+ rowsAffected: number;
10
+ rows: {
11
+ _array: Record<string, import("node:sqlite").SQLOutputValue>[];
12
+ length: number;
13
+ };
14
+ }>;
15
+ executeRaw(query: string, params: any[]): Promise<any[][]>;
16
+ executeBatch(query: string, params: any[][]): Promise<{
17
+ rowsAffected: number;
18
+ }>;
19
+ }
20
+ export declare function openDatabase(worker: PowerSyncWorkerOptions, options: AsyncDatabaseOpenOptions): Promise<BlockingNodeDatabase>;
21
+ export {};
@@ -0,0 +1,46 @@
1
+ import { threadId } from 'node:worker_threads';
2
+ import { dynamicImport } from '../utils/modules.js';
3
+ class BlockingNodeDatabase {
4
+ db;
5
+ constructor(db, write) {
6
+ this.db = db;
7
+ db.function('node_thread_id', () => threadId);
8
+ }
9
+ async close() {
10
+ this.db.close();
11
+ }
12
+ async execute(query, params) {
13
+ const stmt = this.db.prepare(query);
14
+ const rows = stmt.all(...params);
15
+ return {
16
+ rowsAffected: 0,
17
+ rows: {
18
+ _array: rows,
19
+ length: rows.length
20
+ }
21
+ };
22
+ }
23
+ async executeRaw(query, params) {
24
+ const stmt = this.db.prepare(query);
25
+ stmt.setReturnArrays(true); // Missing in @types/node, https://nodejs.org/api/sqlite.html#statementsetreturnarraysenabled
26
+ return stmt.all(...params);
27
+ }
28
+ async executeBatch(query, params) {
29
+ params = params ?? [];
30
+ let rowsAffected = 0;
31
+ const stmt = this.db.prepare(query);
32
+ for (const paramSet of params) {
33
+ const info = stmt.run(...paramSet);
34
+ rowsAffected += info.changes;
35
+ }
36
+ return { rowsAffected };
37
+ }
38
+ }
39
+ export async function openDatabase(worker, options) {
40
+ // NOTE: We want to import node:sqlite dynamically, to avoid bundlers unconditionally requiring node:sqlite in the
41
+ // end, since that would make us incompatible with older Node.JS versions.
42
+ const { DatabaseSync } = await dynamicImport('node:sqlite');
43
+ const baseDB = new DatabaseSync(options.path, { allowExtension: true });
44
+ baseDB.loadExtension(worker.extensionPath(), 'sqlite3_powersync_init');
45
+ return new BlockingNodeDatabase(baseDB, options.isWriter);
46
+ }
@@ -1,7 +1,7 @@
1
1
  import { AbstractPowerSyncDatabase, SqliteBucketStorage } from '@powersync/common';
2
2
  import { NodeRemote } from '../sync/stream/NodeRemote.js';
3
3
  import { NodeStreamingSyncImplementation } from '../sync/stream/NodeStreamingSyncImplementation.js';
4
- import { BetterSQLite3DBAdapter } from './BetterSQLite3DBAdapter.js';
4
+ import { WorkerConnectionPool } from './WorkerConnectionPool.js';
5
5
  /**
6
6
  * A PowerSync database which provides SQLite functionality
7
7
  * which is automatically synced.
@@ -27,7 +27,7 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
27
27
  * Opens a DBAdapter using better-sqlite3 as the default SQLite open factory.
28
28
  */
29
29
  openDBAdapter(options) {
30
- return new BetterSQLite3DBAdapter(options.database);
30
+ return new WorkerConnectionPool(options.database);
31
31
  }
32
32
  generateBucketStorageAdapter() {
33
33
  return new SqliteBucketStorage(this.database, this.logger);
@@ -5,5 +5,13 @@ export interface PowerSyncWorkerOptions {
5
5
  * @returns The absolute path of the PowerSync SQLite core extensions library.
6
6
  */
7
7
  extensionPath: () => string;
8
+ /**
9
+ * A function that returns the `Database` constructor from the `better-sqlite3` package.
10
+ */
11
+ loadBetterSqlite3: () => Promise<any>;
8
12
  }
13
+ /**
14
+ * @returns The relevant PowerSync extension binary filename for the current platform and architecture
15
+ */
16
+ export declare function getPowerSyncExtensionFilename(): string;
9
17
  export declare function startPowerSyncWorker(options?: Partial<PowerSyncWorkerOptions>): void;
@@ -1,126 +1,103 @@
1
- import * as path from 'node:path';
2
- import BetterSQLite3Database from '@powersync/better-sqlite3';
3
1
  import * as Comlink from 'comlink';
4
- import { parentPort, threadId } from 'node:worker_threads';
5
2
  import OS from 'node:os';
3
+ import * as path from 'node:path';
6
4
  import url from 'node:url';
7
- class BlockingAsyncDatabase {
8
- db;
9
- uncommittedUpdatedTables = new Set();
10
- committedUpdatedTables = new Set();
11
- constructor(db) {
12
- this.db = db;
13
- db.function('node_thread_id', () => threadId);
14
- }
15
- collectCommittedUpdates() {
16
- const resolved = Promise.resolve([...this.committedUpdatedTables]);
17
- this.committedUpdatedTables.clear();
18
- return resolved;
19
- }
20
- installUpdateHooks() {
21
- this.db.updateHook((_op, _dbName, tableName, _rowid) => {
22
- this.uncommittedUpdatedTables.add(tableName);
23
- });
24
- this.db.commitHook(() => {
25
- for (const tableName of this.uncommittedUpdatedTables) {
26
- this.committedUpdatedTables.add(tableName);
27
- }
28
- this.uncommittedUpdatedTables.clear();
29
- return true;
30
- });
31
- this.db.rollbackHook(() => {
32
- this.uncommittedUpdatedTables.clear();
33
- });
34
- }
35
- async close() {
36
- this.db.close();
37
- }
38
- async execute(query, params) {
39
- const stmt = this.db.prepare(query);
40
- if (stmt.reader) {
41
- const rows = stmt.all(params);
42
- return {
43
- rowsAffected: 0,
44
- rows: {
45
- _array: rows,
46
- length: rows.length
47
- }
48
- };
5
+ import { parentPort } from 'node:worker_threads';
6
+ import { dynamicImport, isBundledToCommonJs } from '../utils/modules.js';
7
+ import { openDatabase as openBetterSqliteDatabase } from './BetterSqliteWorker.js';
8
+ import { openDatabase as openNodeDatabase } from './NodeSqliteWorker.js';
9
+ /**
10
+ * @returns The relevant PowerSync extension binary filename for the current platform and architecture
11
+ */
12
+ export function getPowerSyncExtensionFilename() {
13
+ const platform = OS.platform();
14
+ const arch = OS.arch();
15
+ let extensionFile;
16
+ if (platform == 'win32') {
17
+ if (arch == 'x64') {
18
+ extensionFile = 'powersync_x64.dll';
19
+ }
20
+ else if (arch == 'ia32') {
21
+ extensionFile = 'powersync_x86.dll';
22
+ }
23
+ else if (arch == 'arm64') {
24
+ extensionFile = 'powersync_aarch64.dll';
49
25
  }
50
26
  else {
51
- const info = stmt.run(params);
52
- return {
53
- rowsAffected: info.changes,
54
- insertId: Number(info.lastInsertRowid)
55
- };
27
+ throw new Error('Windows platform only supports arm64, ia32 and x64 architecture.');
56
28
  }
57
29
  }
58
- async executeRaw(query, params) {
59
- const stmt = this.db.prepare(query);
60
- if (stmt.reader) {
61
- return stmt.raw().all(params);
30
+ else if (platform == 'linux') {
31
+ if (arch == 'x64') {
32
+ extensionFile = 'libpowersync_x64.so';
33
+ }
34
+ else if (arch == 'arm64') {
35
+ // TODO detect armv7 as an option
36
+ extensionFile = 'libpowersync_aarch64.so';
37
+ }
38
+ else if (arch == 'riscv64') {
39
+ extensionFile = 'libpowersync_riscv64gc.so';
62
40
  }
63
41
  else {
64
- stmt.raw().run(params);
65
- return [];
42
+ throw new Error('Linux platform only supports x64, arm64 and riscv64 architectures.');
66
43
  }
67
44
  }
68
- async executeBatch(query, params) {
69
- params = params ?? [];
70
- let rowsAffected = 0;
71
- const stmt = this.db.prepare(query);
72
- for (const paramSet of params) {
73
- const info = stmt.run(paramSet);
74
- rowsAffected += info.changes;
45
+ else if (platform == 'darwin') {
46
+ if (arch == 'x64') {
47
+ extensionFile = 'libpowersync_x64.dylib';
75
48
  }
76
- return { rowsAffected };
77
- }
78
- }
79
- class BetterSqliteWorker {
80
- options;
81
- constructor(options) {
82
- this.options = options;
83
- }
84
- async open(path, isWriter) {
85
- const baseDB = new BetterSQLite3Database(path);
86
- baseDB.pragma('journal_mode = WAL');
87
- baseDB.loadExtension(this.options.extensionPath(), 'sqlite3_powersync_init');
88
- if (!isWriter) {
89
- baseDB.pragma('query_only = true');
49
+ else if (arch == 'arm64') {
50
+ extensionFile = 'libpowersync_aarch64.dylib';
51
+ }
52
+ else {
53
+ throw new Error('macOS platform only supports x64 and arm64 architectures.');
90
54
  }
91
- const asyncDb = new BlockingAsyncDatabase(baseDB);
92
- asyncDb.installUpdateHooks();
93
- return Comlink.proxy(asyncDb);
94
55
  }
56
+ else {
57
+ throw new Error(`Unknown platform: ${platform}, PowerSync for Node.js currently supports Windows, Linux and macOS.`);
58
+ }
59
+ return extensionFile;
95
60
  }
96
61
  export function startPowerSyncWorker(options) {
97
62
  const resolvedOptions = {
98
63
  extensionPath() {
99
- const isCommonJsModule = import.meta.isBundlingToCommonJs ?? false;
100
- const platform = OS.platform();
101
- let extensionPath;
102
- if (platform === 'win32') {
103
- extensionPath = 'powersync.dll';
104
- }
105
- else if (platform === 'linux') {
106
- extensionPath = 'libpowersync.so';
107
- }
108
- else if (platform === 'darwin') {
109
- extensionPath = 'libpowersync.dylib';
110
- }
111
- else {
112
- throw 'Unknown platform, PowerSync for Node.js currently supports Windows, Linux and macOS.';
113
- }
64
+ const isCommonJsModule = isBundledToCommonJs;
65
+ const extensionFilename = getPowerSyncExtensionFilename();
114
66
  let resolved;
115
67
  if (isCommonJsModule) {
116
- resolved = path.resolve(__dirname, '../lib/', extensionPath);
68
+ resolved = path.resolve(__dirname, '../lib/', extensionFilename);
117
69
  }
118
70
  else {
119
- resolved = url.fileURLToPath(new URL(`../${extensionPath}`, import.meta.url));
71
+ resolved = url.fileURLToPath(new URL(`../${extensionFilename}`, import.meta.url));
120
72
  }
121
73
  return resolved;
122
74
  },
75
+ async loadBetterSqlite3() {
76
+ const module = await dynamicImport('better-sqlite3');
77
+ return module.default;
78
+ },
123
79
  ...options
124
80
  };
125
- Comlink.expose(new BetterSqliteWorker(resolvedOptions), parentPort);
81
+ Comlink.expose(new DatabaseOpenHelper(resolvedOptions), parentPort);
82
+ }
83
+ class DatabaseOpenHelper {
84
+ options;
85
+ constructor(options) {
86
+ this.options = options;
87
+ }
88
+ async open(options) {
89
+ let database;
90
+ const implementation = options.implementation;
91
+ switch (implementation.type) {
92
+ case 'better-sqlite3':
93
+ database = await openBetterSqliteDatabase(this.options, options);
94
+ break;
95
+ case 'node:sqlite':
96
+ database = await openNodeDatabase(this.options, options);
97
+ break;
98
+ default:
99
+ throw new Error(`Unknown database implementation: ${options.implementation}.`);
100
+ }
101
+ return Comlink.proxy(database);
102
+ }
126
103
  }
@@ -1,4 +1,4 @@
1
- import { BaseObserver, DBAdapter, DBAdapterListener, LockContext, Transaction, DBLockOptions, QueryResult } from '@powersync/common';
1
+ import { BaseObserver, DBAdapter, DBAdapterListener, DBLockOptions, LockContext, QueryResult, Transaction } from '@powersync/common';
2
2
  import { NodeSQLOpenOptions } from './options.js';
3
3
  export type BetterSQLite3LockContext = LockContext & {
4
4
  executeBatch(query: string, params?: any[][]): Promise<QueryResult>;
@@ -7,7 +7,7 @@ export type BetterSQLite3Transaction = Transaction & BetterSQLite3LockContext;
7
7
  /**
8
8
  * Adapter for better-sqlite3
9
9
  */
10
- export declare class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> implements DBAdapter {
10
+ export declare class WorkerConnectionPool extends BaseObserver<DBAdapterListener> implements DBAdapter {
11
11
  private readonly options;
12
12
  readonly name: string;
13
13
  private readConnections;
@@ -1,15 +1,19 @@
1
+ import * as Comlink from 'comlink';
1
2
  import fs from 'node:fs/promises';
2
3
  import * as path from 'node:path';
3
4
  import { Worker } from 'node:worker_threads';
4
- import * as Comlink from 'comlink';
5
5
  import { BaseObserver } from '@powersync/common';
6
6
  import { AsyncResource } from 'node:async_hooks';
7
+ import { isBundledToCommonJs } from '../utils/modules.js';
7
8
  import { RemoteConnection } from './RemoteConnection.js';
8
9
  const READ_CONNECTIONS = 5;
10
+ const defaultDatabaseImplementation = {
11
+ type: 'better-sqlite3'
12
+ };
9
13
  /**
10
14
  * Adapter for better-sqlite3
11
15
  */
12
- export class BetterSQLite3DBAdapter extends BaseObserver {
16
+ export class WorkerConnectionPool extends BaseObserver {
13
17
  options;
14
18
  name;
15
19
  readConnections;
@@ -42,7 +46,7 @@ export class BetterSQLite3DBAdapter extends BaseObserver {
42
46
  dbFilePath = path.join(this.options.dbLocation, dbFilePath);
43
47
  }
44
48
  const openWorker = async (isWriter) => {
45
- const isCommonJsModule = import.meta.isBundlingToCommonJs ?? false;
49
+ const isCommonJsModule = isBundledToCommonJs;
46
50
  let worker;
47
51
  const workerName = isWriter ? `write ${dbFilePath}` : `read ${dbFilePath}`;
48
52
  const workerFactory = this.options.openWorker ?? ((...args) => new Worker(...args));
@@ -78,8 +82,27 @@ export class BetterSQLite3DBAdapter extends BaseObserver {
78
82
  worker.once('error', (e) => {
79
83
  console.error('Unexpected PowerSync database worker error', e);
80
84
  });
81
- const database = (await comlink.open(dbFilePath, isWriter));
82
- return new RemoteConnection(worker, comlink, database);
85
+ const database = (await comlink.open({
86
+ path: dbFilePath,
87
+ isWriter,
88
+ implementation: this.options.implementation ?? defaultDatabaseImplementation
89
+ }));
90
+ if (isWriter) {
91
+ await database.execute("SELECT powersync_update_hooks('install');", []);
92
+ }
93
+ const connection = new RemoteConnection(worker, comlink, database);
94
+ if (this.options.initializeConnection) {
95
+ await this.options.initializeConnection(connection, isWriter);
96
+ }
97
+ if (!isWriter) {
98
+ await connection.execute('pragma query_only = true');
99
+ }
100
+ else {
101
+ // We only need to enable this on the writer connection.
102
+ // We can get `database is locked` errors if we enable this on concurrently opening read connections.
103
+ await connection.execute('pragma journal_mode = WAL');
104
+ }
105
+ return connection;
83
106
  };
84
107
  // Open the writer first to avoid multiple threads enabling WAL concurrently (causing "database is locked" errors).
85
108
  this.writeConnection = await openWorker(true);
@@ -144,7 +167,8 @@ export class BetterSQLite3DBAdapter extends BaseObserver {
144
167
  return await fn(this.writeConnection);
145
168
  }
146
169
  finally {
147
- const updates = await this.writeConnection.database.collectCommittedUpdates();
170
+ const serializedUpdates = await this.writeConnection.database.executeRaw("SELECT powersync_update_hooks('get');", []);
171
+ const updates = JSON.parse(serializedUpdates[0][0]);
148
172
  if (updates.length > 0) {
149
173
  const event = {
150
174
  tables: updates,