Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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/seven-fireants-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/node': minor
---

Bundle PowerSync extension with NPM package
113 changes: 81 additions & 32 deletions packages/node/download_core.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
// TODO: Make this a pre-publish hook and just bundle everything
import { createHash } from 'node:crypto';
import * as OS from 'node:os';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { Readable } from 'node:stream';
import { finished } from 'node:stream/promises';
import { exit } from 'node:process';

// When changing this version, run node download_core.js update_hashes
const version = '0.4.6';
Expand All @@ -17,33 +14,24 @@ const versionHashes = {
'libpowersync_aarch64.dylib': 'bfb4f1ec207b298aff560f1825f8123d24316edaa27b6df3a17dd49466576b92'
};

const platform = OS.platform();
let destination;
let asset;

if (platform === 'win32') {
asset = 'powersync_x64.dll';
destination = 'powersync.dll';
} else if (platform === 'linux') {
asset = OS.arch() === 'x64' ? 'libpowersync_x64.so' : 'libpowersync_aarch64.so';
destination = 'libpowersync.so';
} else if (platform === 'darwin') {
asset = OS.arch() === 'x64' ? 'libpowersync_x64.dylib' : 'libpowersync_aarch64.dylib';
destination = 'libpowersync.dylib';
}

const expectedHash = versionHashes[asset];
const destinationPath = path.resolve('lib', destination);
// Map of all assets to their destinations
const assetMap = {
'powersync_x64.dll': 'powersync.dll',
'libpowersync_x64.so': 'libpowersync.so',
'libpowersync_aarch64.so': 'libpowersync-aarch64.so',
'libpowersync_x64.dylib': 'libpowersync.dylib',
'libpowersync_aarch64.dylib': 'libpowersync-aarch64.dylib'
};

const hashStream = async (input) => {
for await (const chunk of input.pipe(createHash('sha256')).setEncoding('hex')) {
return chunk;
}
};

const hashLocal = async () => {
const hashLocal = async (filePath) => {
try {
const handle = await fs.open(destinationPath, 'r');
const handle = await fs.open(filePath, 'r');
const input = handle.createReadStream();

const result = await hashStream(input);
Expand All @@ -54,31 +42,92 @@ const hashLocal = async () => {
}
};

const download = async () => {
if ((await hashLocal()) == expectedHash) {
console.debug('Local copy is up-to-date, skipping download');
exit(0);
const downloadAsset = async (asset, destination) => {
const destinationPath = path.resolve('lib', destination);
const expectedHash = versionHashes[asset];

// Check if file exists and has correct hash
const currentHash = await hashLocal(destinationPath);
if (currentHash === expectedHash) {
console.debug(`${destination} is up-to-date, skipping download`);
return;
}

const url = `https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v${version}/${asset}`;
console.log(`Downloading ${url}`);
const response = await fetch(url);
if (response.status != 200) {
throw `Could not download ${url}`;
}

const file = await fs.open(destinationPath, 'w');
await finished(Readable.fromWeb(response.body).pipe(file.createWriteStream()));
await file.close();

const hashAfterDownloading = await hashLocal(destinationPath);
if (hashAfterDownloading != expectedHash) {
throw `Unexpected hash after downloading ${asset} (got ${hashAfterDownloading}, expected ${expectedHash})`;
}
console.log(`Successfully downloaded ${destination}`);
};

const checkAsset = async (asset, destination) => {
const destinationPath = path.resolve('lib', destination);
const expectedHash = versionHashes[asset];
const currentHash = await hashLocal(destinationPath);

return {
asset,
destination,
destinationPath,
expectedHash,
currentHash,
exists: currentHash !== null,
isValid: currentHash === expectedHash
};
};

const download = async () => {
try {
await fs.access('lib');
} catch {
await fs.mkdir('lib');
}

const file = await fs.open(destinationPath, 'w');
await finished(Readable.fromWeb(response.body).pipe(file.createWriteStream()));
await file.close();
// First check all assets
console.log('Checking existing files...');
const checks = await Promise.all(
Object.entries(assetMap).map(([asset, destination]) => checkAsset(asset, destination))
);

const hashAfterDownloading = await hashLocal();
if (hashAfterDownloading != expectedHash) {
throw `Unexpected hash after downloading (got ${hashAfterDownloading}, expected ${expectedHash})`;
const toDownload = checks.filter((check) => !check.isValid);
const upToDate = checks.filter((check) => check.isValid);

// Print summary
if (upToDate.length > 0) {
console.log('\nUp-to-date files:');
for (const check of upToDate) {
console.log(` ✓ ${check.destination}`);
}
}

if (toDownload.length > 0) {
console.log('\nFiles to download:');
for (const check of toDownload) {
if (!check.exists) {
console.log(` • ${check.destination} (missing)`);
} else {
console.log(` • ${check.destination} (hash mismatch)`);
}
}

console.log('\nStarting downloads...');
// Download required assets in parallel
await Promise.all(toDownload.map((check) => downloadAsset(check.asset, check.destination)));

console.log('\nAll downloads completed successfully!');
} else {
console.log('\nAll files are up-to-date, nothing to download.');
}
};

Expand Down
8 changes: 4 additions & 4 deletions packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
"download_core.js"
],
"scripts": {
"install": "node download_core.js",
"build": "tsc -b && rollup --config",
"build:prod": "tsc -b --sourceMap false && rollup --config",
"prepare:core": "node download_core.js",
"build": " pnpm prepare:core && tsc -b && rollup --config",
"build:prod": "pnpm prepare:core && tsc -b --sourceMap false && rollup --config",
"clean": "rm -rf lib dist tsconfig.tsbuildinfo",
"watch": "tsc -b -w",
"test": "vitest",
"test": " pnpm prepare:core && vitest",
"test:exports": "attw --pack . --ignore-rules no-resolution"
},
"type": "module",
Expand Down
28 changes: 23 additions & 5 deletions packages/node/src/db/SqliteWorker.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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';
import OS from 'node:os';
import * as path from 'node:path';
import url from 'node:url';
import { parentPort, threadId } from 'node:worker_threads';
import { AsyncDatabase, AsyncDatabaseOpener } from './AsyncDatabase.js';

class BlockingAsyncDatabase implements AsyncDatabase {
Expand Down Expand Up @@ -129,13 +129,31 @@ export function startPowerSyncWorker(options?: Partial<PowerSyncWorkerOptions>)
const isCommonJsModule = import.meta.isBundlingToCommonJs ?? false;

const platform = OS.platform();
const arch = OS.arch();
let extensionPath: string;

if (platform === 'win32') {
extensionPath = 'powersync.dll';
if (arch === 'x64') {
extensionPath = 'powersync.dll';
} else {
throw 'Windows platform only supports x64 architecture.';
}
} else if (platform === 'linux') {
extensionPath = 'libpowersync.so';
if (arch === 'x64') {
extensionPath = 'libpowersync.so';
} else if (arch === 'arm64') {
extensionPath = 'libpowersync-aarch64.so';
} else {
throw 'Linux platform only supports x64 and arm64 architectures.';
}
} else if (platform === 'darwin') {
extensionPath = 'libpowersync.dylib';
if (arch === 'x64') {
extensionPath = 'libpowersync.dylib';
} else if (arch === 'arm64') {
extensionPath = 'libpowersync-aarch64.dylib';
} else {
throw 'macOS platform only supports x64 and arm64 architectures.';
}
} else {
throw 'Unknown platform, PowerSync for Node.js currently supports Windows, Linux and macOS.';
}
Expand Down
Loading