-
Notifications
You must be signed in to change notification settings - Fork 58
Add electron example using Node.JS SDK in main process #534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 10 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
8586a5a
Start node example running PowerSync in main process
simolus3 f845dd9
Allow compiling custom workers
simolus3 d4cfc25
Add custom worker
simolus3 8c886cb
IPC to talk to PowerSync
simolus3 4f23c33
Set demo as private
simolus3 73ed674
Merge branch 'node-commonjs' into node-electron-sample
simolus3 daebe8b
Bundle powersync loadable
simolus3 bb3aed5
Connect, run query, show results
simolus3 8c5f749
Mention new electron example on readme
simolus3 15cea7a
Mention manual rebuild step
simolus3 5c01894
Support typescript config for older node versions
simolus3 e2b6055
Update demos/example-electron-node/README.md
simolus3 0a46796
Update demos/example-electron-node/README.md
simolus3 7dbecbe
Merge remote-tracking branch 'origin/main' into node-electron-sample
simolus3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Copy to .env.local, and enter your PowerSync instance URL and auth token. | ||
# Leave blank to test local-only. | ||
POWERSYNC_URL= | ||
POWERSYNC_TOKEN= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.webpack/ | ||
out/ | ||
packages/ | ||
.env.local |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# PowerSync + Electron in main process | ||
|
||
This example shows how the [PowerSync Node.js client](https://docs.powersync.com/client-sdk-references/node#node-js-client-alpha) can be used in the main process of an Electron app. | ||
|
||
The purpose of this example is to highlight specific build configurations that enable this setup. | ||
In particular: | ||
|
||
1. In `src/main/index.ts`, a `PowerSyncDatabase` is created. PowerSync uses node workers to speed up database | ||
queries. This worker is part of the `@powersync/node` package and wouldn't be copied into the resulting Electron | ||
app by default. For this reason, this example has its own `src/main/worker.ts` loaded with `new URL('./worker.ts', import.meta.url)`. | ||
2. In addition to the worker, PowerSync requires access to a SQLite extension providing sync functionality. | ||
This file is also part of the `@powersync/node` package and called `powersync.dll`, `libpowersync.dylib` or | ||
`libpowersync.so` depending on the operating system. | ||
We use the `copy-webpack-plugin` package to make sure a copy of that file is available to the main process, | ||
and load it in the custom `src/main/worker.ts`. | ||
3. The `get()` and `getAll()` methods are exposed to the renderer process with an IPC channel. | ||
|
||
To see it in action: | ||
|
||
1. Make sure to run `pnpm install` and `pnpm build:packages` in the root directory of this repo. | ||
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. | ||
The example works with the schema from the [PowerSync + Supabase tutorial](https://docs.powersync.com/integration-guides/supabase-+-powersync#supabase-powersync). | ||
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. | ||
3. Finally, run `pnpm start`. | ||
|
||
Apart from the build setup, this example is purposefully kept simple. | ||
To make sure PowerSync is working, you can run `await powersync.get('SELECT powersync_rs_version()');` in the DevTools | ||
console. A result from that query implies that the PowerSync was properly configured. | ||
|
||
For more details, see the documentation for [the PowerSync node package](https://docs.powersync.com/client-sdk-references/node#node-js-client-alpha) and check other examples: | ||
simolus3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
- [example-node](../example-node/): A Node.js CLI example that connects to PowerSync to run auto-updating queries. | ||
- [example-electron](../example-electron/): An Electron example that runs PowerSync in the render process instead of in the main one. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import OS from 'node:os'; | ||
import path from 'node:path'; | ||
import { createRequire } from 'node:module'; | ||
|
||
import type { ForgeConfig } from '@electron-forge/shared-types'; | ||
import { MakerSquirrel } from '@electron-forge/maker-squirrel'; | ||
import { MakerZIP } from '@electron-forge/maker-zip'; | ||
import { MakerDeb } from '@electron-forge/maker-deb'; | ||
import { MakerRpm } from '@electron-forge/maker-rpm'; | ||
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives'; | ||
import { WebpackPlugin } from '@electron-forge/plugin-webpack'; | ||
import { type Configuration, type ModuleOptions, type DefinePlugin } from 'webpack'; | ||
import * as dotenv from 'dotenv'; | ||
import type IForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; | ||
import type ICopyPlugin from 'copy-webpack-plugin'; | ||
|
||
dotenv.config({path: '.env.local'}); | ||
|
||
const require = createRequire(import.meta.url); | ||
|
||
const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); | ||
const CopyPlugin: typeof ICopyPlugin = require('copy-webpack-plugin'); | ||
const DefinePluginImpl: typeof DefinePlugin = require('webpack').DefinePlugin; | ||
|
||
const webpackPlugins = [ | ||
new ForkTsCheckerWebpackPlugin({ | ||
//logger: 'webpack-infrastructure' | ||
}) | ||
]; | ||
|
||
const defaultWebpackRules: () => Required<ModuleOptions>['rules'] = () => { | ||
return [ | ||
// Add support for native node modules | ||
{ | ||
// We're specifying native_modules in the test because the asset relocator loader generates a | ||
// "fake" .node file which is really a cjs file. | ||
test: /native_modules[/\\].+\.node$/, | ||
use: 'node-loader' | ||
}, | ||
{ | ||
test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, | ||
parser: { amd: false }, | ||
use: { | ||
loader: '@vercel/webpack-asset-relocator-loader', | ||
options: { | ||
outputAssetBase: 'native_modules' | ||
} | ||
} | ||
}, | ||
{ | ||
test: /\.tsx?$/, | ||
exclude: /(node_modules|\.webpack)/, | ||
use: { | ||
loader: 'ts-loader', | ||
options: { | ||
transpileOnly: true | ||
} | ||
} | ||
} | ||
]; | ||
}; | ||
|
||
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.'; | ||
} | ||
|
||
const mainConfig: Configuration = { | ||
/** | ||
* This is the main entry point for your application, it's the first file | ||
* that runs in the main process. | ||
*/ | ||
entry: './src/main/index.ts', | ||
// Put your normal webpack config below here | ||
module: { | ||
rules: defaultWebpackRules(), | ||
}, | ||
plugins: [ | ||
...webpackPlugins, | ||
new CopyPlugin({ | ||
patterns: [{ | ||
from: path.resolve(require.resolve('@powersync/node/package.json'), `../lib/${extensionPath}`), | ||
to: extensionPath, | ||
}], | ||
}), | ||
new DefinePluginImpl({ | ||
POWERSYNC_URL: JSON.stringify(process.env.POWERSYNC_URL), | ||
POWERSYNC_TOKEN: JSON.stringify(process.env.POWERSYNC_TOKEN), | ||
}), | ||
], | ||
resolve: { | ||
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'] | ||
} | ||
}; | ||
|
||
const rendererConfig: Configuration = { | ||
module: { | ||
rules: [ | ||
...defaultWebpackRules(), | ||
{ | ||
test: /\.css$/, | ||
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] | ||
} | ||
], | ||
}, | ||
plugins: webpackPlugins, | ||
resolve: { | ||
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'] | ||
} | ||
}; | ||
|
||
const config: ForgeConfig = { | ||
packagerConfig: { | ||
asar: true | ||
}, | ||
rebuildConfig: { | ||
force: true, | ||
}, | ||
makers: [ | ||
new MakerSquirrel(), | ||
new MakerZIP({}, ['darwin']), | ||
new MakerRpm({ options: { icon: './public/icons/icon' } }), | ||
new MakerDeb({ options: { icon: './public/icons/icon' } }) | ||
], | ||
plugins: [ | ||
new AutoUnpackNativesPlugin({}), | ||
new WebpackPlugin({ | ||
mainConfig, | ||
renderer: { | ||
config: rendererConfig, | ||
entryPoints: [ | ||
{ | ||
name: 'main_window', | ||
html: './src/render/index.html', | ||
js: './src/render/main.ts', | ||
preload: { | ||
js: './src/render/preload.ts', | ||
} | ||
} | ||
] | ||
} | ||
}) | ||
] | ||
}; | ||
|
||
export default config; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{ | ||
"name": "example-electron-node", | ||
"version": "1.0.0", | ||
"description": "", | ||
"keywords": [], | ||
"packageManager": "[email protected]", | ||
"main": ".webpack/main", | ||
"private": true, | ||
"author": { | ||
"name": "PowerSync" | ||
}, | ||
"license": "MIT", | ||
"scripts": { | ||
"start": "electron-forge start", | ||
"package": "electron-forge package", | ||
"make": "electron-forge make", | ||
"publish": "electron-forge publish" | ||
}, | ||
"devDependencies": { | ||
"@electron-forge/cli": "^7.7.0", | ||
"@electron-forge/maker-deb": "^7.7.0", | ||
"@electron-forge/maker-squirrel": "^7.7.0", | ||
"@electron-forge/maker-zip": "^7.7.0", | ||
"@electron-forge/plugin-auto-unpack-natives": "^7.7.0", | ||
"@electron-forge/plugin-webpack": "^7.7.0", | ||
"@vercel/webpack-asset-relocator-loader": "1.7.3", | ||
"copy-webpack-plugin": "^13.0.0", | ||
"css-loader": "^6.11.0", | ||
"dotenv": "^16.4.7", | ||
"electron": "30.0.2", | ||
"electron-rebuild": "^3.2.9", | ||
"fork-ts-checker-webpack-plugin": "^9.0.2", | ||
"node-loader": "^2.1.0", | ||
"style-loader": "^3.3.4", | ||
"ts-loader": "^9.5.2", | ||
"ts-node": "^10.9.2", | ||
"typescript": "^5.8.2", | ||
"webpack": "^5.90.1" | ||
}, | ||
"dependencies": { | ||
"@powersync/node": "workspace:*", | ||
"electron-squirrel-startup": "^1.0.1" | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { Worker } from 'node:worker_threads'; | ||
|
||
import { PowerSyncDatabase, SyncStreamConnectionMethod } from '@powersync/node'; | ||
import { app, BrowserWindow, ipcMain, MessagePortMain } from 'electron'; | ||
import { AppSchema, BackendConnector } from './powersync'; | ||
import { default as Logger } from 'js-logger'; | ||
|
||
const logger = Logger.get('PowerSyncDemo'); | ||
Logger.useDefaults({ defaultLevel: logger.WARN }); | ||
|
||
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack | ||
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on | ||
// whether you're running in development or production). | ||
declare const MAIN_WINDOW_WEBPACK_ENTRY: string; | ||
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string; | ||
|
||
// Handle creating/removing shortcuts on Windows when installing/uninstalling. | ||
if (require('electron-squirrel-startup')) { | ||
app.quit(); | ||
} | ||
|
||
const database = new PowerSyncDatabase({ | ||
schema: AppSchema, | ||
database: { | ||
dbFilename: 'test.db', | ||
openWorker(_, options) { | ||
return new Worker(new URL('./worker.ts', import.meta.url), options); | ||
} | ||
}, | ||
logger | ||
}); | ||
|
||
const createWindow = (): void => { | ||
// Create the browser window. | ||
const mainWindow = new BrowserWindow({ | ||
height: 600, | ||
width: 800, | ||
webPreferences: { | ||
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY | ||
} | ||
}); | ||
|
||
// and load the index.html of the app. | ||
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY); | ||
|
||
// Open the DevTools. | ||
mainWindow.webContents.openDevTools(); | ||
}; | ||
|
||
// This method will be called when Electron has finished | ||
// initialization and is ready to create browser windows. | ||
// Some APIs can only be used after this event occurs. | ||
app.whenReady().then(() => { | ||
database.connect(new BackendConnector(), { connectionMethod: SyncStreamConnectionMethod.HTTP }); | ||
|
||
const forwardSyncStatus = (port: MessagePortMain) => { | ||
port.postMessage(database.currentStatus.toJSON()); | ||
const unregister = database.registerListener({ | ||
statusChanged(status) { | ||
port.postMessage(status.toJSON()); | ||
}, | ||
}); | ||
port.once('close', unregister); | ||
}; | ||
|
||
const forwardWatchResults = (sql: string, args: any[], port: MessagePortMain) => { | ||
const abort = new AbortController(); | ||
port.once('close', () => abort.abort()); | ||
|
||
database.watchWithCallback(sql, args, { | ||
onResult(results) { | ||
port.postMessage(results.rows._array); | ||
}, | ||
onError(error) { | ||
console.error(`Watch ${sql} with ${args} failed`, error); | ||
}, | ||
}, {signal: abort.signal}); | ||
}; | ||
|
||
ipcMain.on('port', (portEvent) => { | ||
const [port] = portEvent.ports; | ||
port.start(); | ||
|
||
port.on('message', (event) => { | ||
const {method, payload} = event.data; | ||
switch (method) { | ||
case 'syncStatus': | ||
forwardSyncStatus(port); | ||
break; | ||
case 'watch': | ||
const {sql, args} = payload; | ||
forwardWatchResults(sql, args, port); | ||
break; | ||
}; | ||
}); | ||
}); | ||
|
||
ipcMain.handle('get', async (_, sql: string, args: any[]) => { | ||
return await database.get(sql, args); | ||
}); | ||
ipcMain.handle('getAll', async (_, sql: string, args: any[]) => { | ||
return await database.getAll(sql, args); | ||
}); | ||
createWindow(); | ||
}); | ||
|
||
// Quit when all windows are closed, except on macOS. There, it's common | ||
// for applications and their menu bar to stay active until the user quits | ||
// explicitly with Cmd + Q. | ||
app.on('window-all-closed', () => { | ||
if (process.platform !== 'darwin') { | ||
app.quit(); | ||
} | ||
}); | ||
|
||
app.on('activate', () => { | ||
// On OS X it's common to re-create a window in the app when the | ||
// dock icon is clicked and there are no other windows open. | ||
if (BrowserWindow.getAllWindows().length === 0) { | ||
createWindow(); | ||
} | ||
}); | ||
|
||
// In this file you can include the rest of your app's specific main process | ||
// code. You can also put them in separate files and import them here. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.