nodejs: load libsimplex at runtime per backend

This commit is contained in:
shum committed 2026-09-25 12:51:14 +00:00
1 parent a5ceeac000
commit 80182a7237
49 files changed
+549 -495

No files matched your search

+4 -5
View File
@@ -57,10 +57,8 @@ interface EventSubscriber<K extends CEvt.Tag> {
}
/**
* Database configuration. The native library is built against exactly one
* backend (see `simplex_backend` / `SIMPLEX_BACKEND` at install time); this
* type makes the caller state which one they are targeting so field names
* can't lie about their meaning.
* Database configuration. `type` selects the libsimplex backend loaded by
* `ChatApi.init`; one backend per process.
*/
export type DbConfig =
| {
@@ -103,7 +101,7 @@ export class ChatApi {
private constructor(protected ctrl_: bigint | undefined) {}
/**
* Initializes the ChatApi.
* Initializes the ChatApi, loading libsimplex for `db.type` (downloaded on first use).
* @param {DbConfig} db - Database configuration (sqlite or postgres).
* @param {core.MigrationConfirmation} [confirm=core.MigrationConfirmation.YesUp] - Migration confirmation mode.
* @param {number} [queueSize] - Size of internal queues, the core default is used when omitted.
@@ -114,6 +112,7 @@ export class ChatApi {
queueSize?: number
): Promise<ChatApi> {
const [path, key] = dbConfigToMigrateArgs(db)
await core.loadLibrary(db.type)
const ctrl = await core.chatMigrateInit(path, key, confirm, queueSize)
return new ChatApi(ctrl)
}
+19
View File
@@ -1,8 +1,27 @@
import {ChatEvent, ChatResponse, T} from "@simplex-chat/types"
import * as libs from "./libs"
import * as simplex from "./simplex"
export type Backend = "sqlite" | "postgres"
let loading: {backend: Backend, promise: Promise<void>} | undefined
/**
* Resolve (downloading on first use) and load libsimplex for the backend.
* One libsimplex per process: the Haskell runtime is initialized once, and another backend is rejected.
*/
export function loadLibrary(backend: Backend): Promise<void> {
if (loading) {
if (loading.backend === backend) return loading.promise
return Promise.reject(new Error(`libsimplex already loaded with backend=${loading.backend}; cannot switch to ${backend} in the same process`))
}
const promise = libs.resolveLibsDir(backend).then(dir => simplex.load(libs.libPath(dir)))
loading = {backend, promise}
// a failed download or load can be retried
promise.catch(() => { loading = undefined })
return promise
}
/**
* Initialize chat controller
* @param {number} [queueSize] - Size of internal queues, the core default is used when omitted.
@@ -1,266 +0,0 @@
const https = require('https');
const fs = require('fs');
const path = require('path');
const extract = require('extract-zip');
const GITHUB_REPO = 'simplex-chat/simplex-chat-libs';
const RELEASE_TAG = 'v7.1.0-beta.4';
const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase();
// A locally built libsimplex, copied into libs/ rather than loaded in place:
// the addon's RUNPATH is $ORIGIN/../../libs.
const LIBS_DIR_OVERRIDE = process.env.SIMPLEX_LIBS_DIR || process.env.npm_config_simplex_libs_dir;
const LIB_NAMES = ['libsimplex.so', 'libsimplex.dylib', 'libsimplex.dll'];
if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') {
console.error(`✗ Invalid SIMPLEX_BACKEND: "${BACKEND}". Must be "sqlite" or "postgres".`);
process.exit(1);
}
if (BACKEND === 'postgres' && (process.platform !== 'linux' || process.arch !== 'x64')) {
console.error(`✗ SIMPLEX_BACKEND=postgres is only supported on Linux x86_64.`);
process.exit(1);
}
const ROOT_DIR = process.cwd(); // Root of the package being installed
const LIBS_DIR = path.join(ROOT_DIR, 'libs')
const INSTALLED_FILE = path.join(LIBS_DIR, 'installed.txt');
// Detect platform and architecture
function getPlatformInfo() {
const platform = process.platform;
const arch = process.arch;
let platformName;
let archName;
if (platform === 'linux') {
platformName = 'linux';
} else if (platform === 'darwin') {
platformName = 'macos';
} else if (platform === 'win32') {
platformName = 'windows';
} else {
throw new Error(`Unsupported platform: ${platform}`);
}
if (arch === 'x64') {
archName = 'x86_64';
} else if (arch === 'arm64') {
archName = 'aarch64';
} else {
throw new Error(`Unsupported architecture: ${arch}`);
}
return { platformName, archName };
}
// Cleanup on libs version mismatch
function cleanLibsDirectory() {
if (fs.existsSync(LIBS_DIR)) {
console.log('Cleaning old libraries...');
fs.rmSync(LIBS_DIR, { recursive: true, force: true });
fs.mkdirSync(LIBS_DIR, { recursive: true });
console.log('✓ Old libraries removed');
}
}
// Check if libraries are already installed with the correct version
function isAlreadyInstalled() {
if (!fs.existsSync(INSTALLED_FILE)) {
return false;
}
try {
const installedVersion = fs.readFileSync(INSTALLED_FILE, 'utf-8').trim();
const expectedVersion = `${RELEASE_TAG}:${BACKEND}`;
if (installedVersion === expectedVersion) {
console.log(`✓ Libraries version ${RELEASE_TAG}:${BACKEND} already installed`);
return true;
} else {
console.log(`Version mismatch: installed ${installedVersion}, need ${expectedVersion}`);
cleanLibsDirectory();
return false;
}
} catch (err) {
console.warn(`Could not read installed.txt: ${err.message}`);
return false;
}
}
// No version check: the files behind SIMPLEX_LIBS_DIR change on every rebuild.
function installFromOverride() {
if (!fs.existsSync(LIBS_DIR_OVERRIDE)) {
throw new Error(`SIMPLEX_LIBS_DIR does not exist: ${LIBS_DIR_OVERRIDE}`);
}
const lib = LIB_NAMES.find((name) => fs.existsSync(path.join(LIBS_DIR_OVERRIDE, name)));
if (!lib) {
throw new Error(`No ${LIB_NAMES.join(' / ')} in SIMPLEX_LIBS_DIR: ${LIBS_DIR_OVERRIDE}`);
}
console.log(`Using libraries from SIMPLEX_LIBS_DIR: ${LIBS_DIR_OVERRIDE}`);
cleanLibsDirectory();
copyDirSync(LIBS_DIR_OVERRIDE, LIBS_DIR);
// Not a release tag: a later install without the variable sees a mismatch
// and replaces these files with the released ones.
fs.writeFileSync(INSTALLED_FILE, `${LIBS_DIR_OVERRIDE}:${BACKEND}`, 'utf-8');
console.log(`✓ Installed ${lib} and its runtime libraries from ${LIBS_DIR_OVERRIDE}`);
}
async function install() {
try {
if (LIBS_DIR_OVERRIDE) {
installFromOverride();
return;
}
// Check if already installed
if (isAlreadyInstalled()) {
return;
}
const { platformName, archName } = getPlatformInfo();
const repoName = GITHUB_REPO.split('/')[1];
const backendSuffix = BACKEND === 'postgres' ? '-postgres' : '';
const zipFilename = `${repoName}-${platformName}-${archName}${backendSuffix}.zip`;
const ZIP_URL = `https://github.com/${GITHUB_REPO}/releases/download/${RELEASE_TAG}/${zipFilename}`;
const ZIP_PATH = path.join(ROOT_DIR, zipFilename);
const TEMP_EXTRACT_DIR = path.join(ROOT_DIR, '.temp-extract');
console.log(`Detected: ${platformName} ${archName}`);
console.log(`Backend: ${BACKEND}`);
console.log(`Downloading: ${zipFilename}`);
// Create libs directory
if (!fs.existsSync(LIBS_DIR)) {
fs.mkdirSync(LIBS_DIR, { recursive: true });
}
// Download zip with error handling
await downloadFile(ZIP_URL, ZIP_PATH);
// Extract to temporary directory
console.log('Extracting to temporary directory...');
if (!fs.existsSync(TEMP_EXTRACT_DIR)) {
fs.mkdirSync(TEMP_EXTRACT_DIR, { recursive: true });
}
await extract(ZIP_PATH, { dir: TEMP_EXTRACT_DIR });
// Move libs folder contents to final location
console.log('Moving libraries to libs/...');
const libsSourcePath = path.join(TEMP_EXTRACT_DIR, 'libs');
if (fs.existsSync(libsSourcePath)) {
// Copy all files from libs folder to LIBS_DIR
const files = fs.readdirSync(libsSourcePath);
files.forEach(file => {
const src = path.join(libsSourcePath, file);
const dest = path.join(LIBS_DIR, file);
if (fs.statSync(src).isDirectory()) {
copyDirSync(src, dest);
} else {
fs.copyFileSync(src, dest);
}
});
} else {
throw new Error('libs folder not found in zip archive');
}
// Write installed.txt with version
fs.writeFileSync(INSTALLED_FILE, `${RELEASE_TAG}:${BACKEND}`, 'utf-8');
console.log(`✓ Wrote version ${RELEASE_TAG}:${BACKEND} to installed.txt`);
// Cleanup
fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true });
fs.unlinkSync(ZIP_PATH);
console.log('✓ Installation complete');
} catch (err) {
console.error('✗ Failed:', err.message);
process.exit(1);
}
}
// Helper function to recursively copy directories
function copyDirSync(src, dest) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const files = fs.readdirSync(src);
files.forEach(file => {
const srcFile = path.join(src, file);
const destFile = path.join(dest, file);
if (fs.statSync(srcFile).isDirectory()) {
copyDirSync(srcFile, destFile);
} else {
fs.copyFileSync(srcFile, destFile);
}
});
}
function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
https.get(url, { headers: { 'User-Agent': 'Node.js' } }, (response) => {
// Handle redirects
if (response.statusCode === 302 || response.statusCode === 301) {
file.destroy();
fs.unlink(dest, () => {});
return downloadFile(response.headers.location, dest)
.then(resolve)
.catch(reject);
}
// Handle 404
if (response.statusCode === 404) {
file.destroy();
fs.unlink(dest, () => {});
reject(new Error(
`Release artifact not found (404). Check:\n` +
` - Repository exists: ${url.split('/releases')[0]}\n` +
` - Release tag exists: ${RELEASE_TAG}\n` +
` - Artifact filename is correct`
));
return;
}
// Handle 403
if (response.statusCode === 403) {
file.destroy();
fs.unlink(dest, () => {});
reject(new Error(
`Access denied (403). The repository may be private.\n` +
`Set GITHUB_TOKEN environment variable for private repos.`
));
return;
}
// Handle other HTTP errors
if (response.statusCode < 200 || response.statusCode >= 300) {
file.destroy();
fs.unlink(dest, () => {});
reject(new Error(
`HTTP ${response.statusCode}: Failed to download from ${url}`
));
return;
}
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
file.on('error', (err) => {
fs.unlink(dest, () => {});
reject(new Error(`File write error: ${err.message}`));
});
}).on('error', (err) => {
file.destroy();
fs.unlink(dest, () => {});
reject(new Error(`Download error: ${err.message}`));
});
});
}
install();
+1
View File
@@ -1,5 +1,6 @@
// These functions are defined in CPP add-on ../cpp/simplex.cc
export function load(libPath: string): void
export function chat_migrate_init(dbPath: string, dbKey: string, confirm: string): Promise<[bigint, string]>
export function chat_migrate_init_queue(dbPath: string, dbKey: string, confirm: string, queueSize: number): Promise<[bigint, string]>
export function chat_close_store(ctrl: bigint): Promise<string>