Skip to content

Commit 7d2a67b

Browse files
authored
Merge pull request #534 from powersync-ja/node-electron-sample
Add electron example using Node.JS SDK in main process
2 parents 098934a + 7dbecbe commit 7d2a67b

File tree

16 files changed

+1587
-164
lines changed

16 files changed

+1587
-164
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ Demo applications are located in the [`demos/`](./demos/) directory. Also see ou
7979

8080
### Electron
8181

82-
- [demos/example-electron](./demos/example-electron/README.md) An Electron example web rendered app using the PowerSync Web SDK.
82+
- [demos/example-electron](./demos/example-electron/README.md) An Electron example web rendered app using the PowerSync Web SDK in the renderer process.
83+
- [demos/example-electron-node](./demos/example-electron-node/README.md) An Electron example using a PowerSync database in the main process.
8384

8485
### Capacitor
8586

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copy to .env.local, and enter your PowerSync instance URL and auth token.
2+
# Leave blank to test local-only.
3+
POWERSYNC_URL=
4+
POWERSYNC_TOKEN=
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.webpack/
2+
out/
3+
packages/
4+
.env.local

demos/example-electron-node/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# PowerSync + Electron in main process
2+
3+
This example shows how the [PowerSync Node.js client](https://docs.powersync.com/client-sdk-references/node) can be used in the main process of an Electron app.
4+
5+
The purpose of this example is to highlight specific build configurations that enable this setup.
6+
In particular:
7+
8+
1. In `src/main/index.ts`, a `PowerSyncDatabase` is created. PowerSync uses node workers to speed up database
9+
queries. This worker is part of the `@powersync/node` package and wouldn't be copied into the resulting Electron
10+
app by default. For this reason, this example has its own `src/main/worker.ts` loaded with `new URL('./worker.ts', import.meta.url)`.
11+
2. In addition to the worker, PowerSync requires access to a SQLite extension providing sync functionality.
12+
This file is also part of the `@powersync/node` package and called `powersync.dll`, `libpowersync.dylib` or
13+
`libpowersync.so` depending on the operating system.
14+
We use the `copy-webpack-plugin` package to make sure a copy of that file is available to the main process,
15+
and load it in the custom `src/main/worker.ts`.
16+
3. The `get()` and `getAll()` methods are exposed to the renderer process with an IPC channel.
17+
18+
To see it in action:
19+
20+
1. Make sure to run `pnpm install` and `pnpm build:packages` in the root directory of this repo.
21+
2. Copy `.env.local.template` to `.env.local`, and complete the environment variables. You can generate a [temporary development token](https://docs.powersync.com/usage/installation/authentication-setup/development-tokens), or leave blank to test with local-only data.
22+
The example works with the schema from the [PowerSync + Supabase tutorial](https://docs.powersync.com/integration-guides/supabase-+-powersync#supabase-powersync).
23+
3. `cd` into this directory. In this mono-repo, you'll have to run `./node_modules/.bin/electron-rebuild` once to make sure `@powersync/better-sqlite3` was compiled with Electron's toolchain.
24+
3. Finally, run `pnpm start`.
25+
26+
Apart from the build setup, this example is purposefully kept simple.
27+
To make sure PowerSync is working, you can run `await powersync.get('SELECT powersync_rs_version()');` in the DevTools
28+
console. A result from that query implies that the PowerSync was properly configured.
29+
30+
For more details, see the documentation for [the PowerSync node package](https://docs.powersync.com/client-sdk-references/node) and check other examples:
31+
32+
- [example-node](../example-node/): A Node.js CLI example that connects to PowerSync to run auto-updating queries.
33+
- [example-electron](../example-electron/): An Electron example that runs PowerSync in the render process instead of in the main one.

demos/example-electron-node/config.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import OS from 'node:os';
2+
import path from 'node:path';
3+
import { createRequire } from 'node:module';
4+
5+
import type { ForgeConfig } from '@electron-forge/shared-types';
6+
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
7+
import { MakerZIP } from '@electron-forge/maker-zip';
8+
import { MakerDeb } from '@electron-forge/maker-deb';
9+
import { MakerRpm } from '@electron-forge/maker-rpm';
10+
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
11+
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
12+
import { type Configuration, type ModuleOptions, type DefinePlugin } from 'webpack';
13+
import * as dotenv from 'dotenv';
14+
import type IForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
15+
import type ICopyPlugin from 'copy-webpack-plugin';
16+
17+
dotenv.config({path: '.env.local'});
18+
19+
const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
20+
const CopyPlugin: typeof ICopyPlugin = require('copy-webpack-plugin');
21+
const DefinePluginImpl: typeof DefinePlugin = require('webpack').DefinePlugin;
22+
23+
const webpackPlugins = [
24+
new ForkTsCheckerWebpackPlugin({
25+
//logger: 'webpack-infrastructure'
26+
})
27+
];
28+
29+
const defaultWebpackRules: () => Required<ModuleOptions>['rules'] = () => {
30+
return [
31+
// Add support for native node modules
32+
{
33+
// We're specifying native_modules in the test because the asset relocator loader generates a
34+
// "fake" .node file which is really a cjs file.
35+
test: /native_modules[/\\].+\.node$/,
36+
use: 'node-loader'
37+
},
38+
{
39+
test: /[/\\]node_modules[/\\].+\.(m?js|node)$/,
40+
parser: { amd: false },
41+
use: {
42+
loader: '@vercel/webpack-asset-relocator-loader',
43+
options: {
44+
outputAssetBase: 'native_modules'
45+
}
46+
}
47+
},
48+
{
49+
test: /\.tsx?$/,
50+
exclude: /(node_modules|\.webpack)/,
51+
use: {
52+
loader: 'ts-loader',
53+
options: {
54+
transpileOnly: true
55+
}
56+
}
57+
}
58+
];
59+
};
60+
61+
const platform = OS.platform();
62+
let extensionPath: string;
63+
if (platform === 'win32') {
64+
extensionPath = 'powersync.dll';
65+
} else if (platform === 'linux') {
66+
extensionPath = 'libpowersync.so';
67+
} else if (platform === 'darwin') {
68+
extensionPath = 'libpowersync.dylib';
69+
} else {
70+
throw 'Unknown platform, PowerSync for Node.js currently supports Windows, Linux and macOS.';
71+
}
72+
73+
const mainConfig: Configuration = {
74+
/**
75+
* This is the main entry point for your application, it's the first file
76+
* that runs in the main process.
77+
*/
78+
entry: './src/main/index.ts',
79+
// Put your normal webpack config below here
80+
module: {
81+
rules: defaultWebpackRules(),
82+
},
83+
plugins: [
84+
...webpackPlugins,
85+
new CopyPlugin({
86+
patterns: [{
87+
from: path.resolve(require.resolve('@powersync/node/package.json'), `../lib/${extensionPath}`),
88+
to: extensionPath,
89+
}],
90+
}),
91+
new DefinePluginImpl({
92+
POWERSYNC_URL: JSON.stringify(process.env.POWERSYNC_URL),
93+
POWERSYNC_TOKEN: JSON.stringify(process.env.POWERSYNC_TOKEN),
94+
}),
95+
],
96+
resolve: {
97+
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json']
98+
}
99+
};
100+
101+
const rendererConfig: Configuration = {
102+
module: {
103+
rules: [
104+
...defaultWebpackRules(),
105+
{
106+
test: /\.css$/,
107+
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }]
108+
}
109+
],
110+
},
111+
plugins: webpackPlugins,
112+
resolve: {
113+
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css']
114+
}
115+
};
116+
117+
const config: ForgeConfig = {
118+
packagerConfig: {
119+
asar: true
120+
},
121+
rebuildConfig: {
122+
force: true,
123+
},
124+
makers: [
125+
new MakerSquirrel(),
126+
new MakerZIP({}, ['darwin']),
127+
new MakerRpm({ options: { icon: './public/icons/icon' } }),
128+
new MakerDeb({ options: { icon: './public/icons/icon' } })
129+
],
130+
plugins: [
131+
new AutoUnpackNativesPlugin({}),
132+
new WebpackPlugin({
133+
mainConfig,
134+
renderer: {
135+
config: rendererConfig,
136+
entryPoints: [
137+
{
138+
name: 'main_window',
139+
html: './src/render/index.html',
140+
js: './src/render/main.ts',
141+
preload: {
142+
js: './src/render/preload.ts',
143+
}
144+
}
145+
]
146+
}
147+
})
148+
]
149+
};
150+
151+
export default config;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
require('tsx/cjs');
2+
3+
module.exports = require('./config.ts');
4+
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "example-electron-node",
3+
"version": "1.0.0",
4+
"description": "",
5+
"keywords": [],
6+
"packageManager": "[email protected]",
7+
"main": ".webpack/main",
8+
"private": true,
9+
"author": {
10+
"name": "PowerSync"
11+
},
12+
"license": "MIT",
13+
"scripts": {
14+
"start": "electron-forge start",
15+
"package": "electron-forge package",
16+
"make": "electron-forge make",
17+
"publish": "electron-forge publish"
18+
},
19+
"devDependencies": {
20+
"@electron-forge/cli": "^7.7.0",
21+
"@electron-forge/maker-deb": "^7.7.0",
22+
"@electron-forge/maker-squirrel": "^7.7.0",
23+
"@electron-forge/maker-zip": "^7.7.0",
24+
"@electron-forge/plugin-auto-unpack-natives": "^7.7.0",
25+
"@electron-forge/plugin-webpack": "^7.7.0",
26+
"@vercel/webpack-asset-relocator-loader": "1.7.3",
27+
"copy-webpack-plugin": "^13.0.0",
28+
"css-loader": "^6.11.0",
29+
"dotenv": "^16.4.7",
30+
"electron": "30.0.2",
31+
"electron-rebuild": "^3.2.9",
32+
"fork-ts-checker-webpack-plugin": "^9.0.2",
33+
"node-loader": "^2.1.0",
34+
"style-loader": "^3.3.4",
35+
"ts-loader": "^9.5.2",
36+
"ts-node": "^10.9.2",
37+
"tsx": "^4.19.3",
38+
"typescript": "^5.8.2",
39+
"webpack": "^5.90.1"
40+
},
41+
"dependencies": {
42+
"@powersync/node": "workspace:*",
43+
"electron-squirrel-startup": "^1.0.1"
44+
}
45+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Worker } from 'node:worker_threads';
2+
3+
import { PowerSyncDatabase, SyncStreamConnectionMethod } from '@powersync/node';
4+
import { app, BrowserWindow, ipcMain, MessagePortMain } from 'electron';
5+
import { AppSchema, BackendConnector } from './powersync';
6+
import { default as Logger } from 'js-logger';
7+
8+
const logger = Logger.get('PowerSyncDemo');
9+
Logger.useDefaults({ defaultLevel: logger.WARN });
10+
11+
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
12+
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
13+
// whether you're running in development or production).
14+
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
15+
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
16+
17+
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
18+
if (require('electron-squirrel-startup')) {
19+
app.quit();
20+
}
21+
22+
const database = new PowerSyncDatabase({
23+
schema: AppSchema,
24+
database: {
25+
dbFilename: 'test.db',
26+
openWorker(_, options) {
27+
return new Worker(new URL('./worker.ts', import.meta.url), options);
28+
}
29+
},
30+
logger
31+
});
32+
33+
const createWindow = (): void => {
34+
// Create the browser window.
35+
const mainWindow = new BrowserWindow({
36+
height: 600,
37+
width: 800,
38+
webPreferences: {
39+
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY
40+
}
41+
});
42+
43+
// and load the index.html of the app.
44+
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
45+
46+
// Open the DevTools.
47+
mainWindow.webContents.openDevTools();
48+
};
49+
50+
// This method will be called when Electron has finished
51+
// initialization and is ready to create browser windows.
52+
// Some APIs can only be used after this event occurs.
53+
app.whenReady().then(() => {
54+
database.connect(new BackendConnector(), { connectionMethod: SyncStreamConnectionMethod.HTTP });
55+
56+
const forwardSyncStatus = (port: MessagePortMain) => {
57+
port.postMessage(database.currentStatus.toJSON());
58+
const unregister = database.registerListener({
59+
statusChanged(status) {
60+
port.postMessage(status.toJSON());
61+
},
62+
});
63+
port.once('close', unregister);
64+
};
65+
66+
const forwardWatchResults = (sql: string, args: any[], port: MessagePortMain) => {
67+
const abort = new AbortController();
68+
port.once('close', () => abort.abort());
69+
70+
database.watchWithCallback(sql, args, {
71+
onResult(results) {
72+
port.postMessage(results.rows._array);
73+
},
74+
onError(error) {
75+
console.error(`Watch ${sql} with ${args} failed`, error);
76+
},
77+
}, {signal: abort.signal});
78+
};
79+
80+
ipcMain.on('port', (portEvent) => {
81+
const [port] = portEvent.ports;
82+
port.start();
83+
84+
port.on('message', (event) => {
85+
const {method, payload} = event.data;
86+
switch (method) {
87+
case 'syncStatus':
88+
forwardSyncStatus(port);
89+
break;
90+
case 'watch':
91+
const {sql, args} = payload;
92+
forwardWatchResults(sql, args, port);
93+
break;
94+
};
95+
});
96+
});
97+
98+
ipcMain.handle('get', async (_, sql: string, args: any[]) => {
99+
return await database.get(sql, args);
100+
});
101+
ipcMain.handle('getAll', async (_, sql: string, args: any[]) => {
102+
return await database.getAll(sql, args);
103+
});
104+
createWindow();
105+
});
106+
107+
// Quit when all windows are closed, except on macOS. There, it's common
108+
// for applications and their menu bar to stay active until the user quits
109+
// explicitly with Cmd + Q.
110+
app.on('window-all-closed', () => {
111+
if (process.platform !== 'darwin') {
112+
app.quit();
113+
}
114+
});
115+
116+
app.on('activate', () => {
117+
// On OS X it's common to re-create a window in the app when the
118+
// dock icon is clicked and there are no other windows open.
119+
if (BrowserWindow.getAllWindows().length === 0) {
120+
createWindow();
121+
}
122+
});
123+
124+
// In this file you can include the rest of your app's specific main process
125+
// code. You can also put them in separate files and import them here.

0 commit comments

Comments
 (0)