Skip to content

Commit bb3aed5

Browse files
committed
Connect, run query, show results
1 parent daebe8b commit bb3aed5

File tree

11 files changed

+206
-46
lines changed

11 files changed

+206
-46
lines changed
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.webpack/
22
out/
33
packages/
4+
.env.local
Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
1-
TODO:
1+
# PowerSync + Electron in main process
22

3-
- `electron-rebuild` needs to run.
4-
- Explain custom worker.
3+
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.
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` in this directory and run `pnpm start`.
24+
25+
Apart from the build setup, this example is purposefully kept simple.
26+
To make sure PowerSync is working, you can run `await powersync.get('SELECT powersync_rs_version()');` in the DevTools
27+
console. A result from that query implies that the PowerSync was properly configured.
28+
29+
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:
30+
31+
- [example-node](../example-node/): A Node.js CLI example that connects to PowerSync to run auto-updating queries.
32+
- [example-electron](../example-electron/): An Electron example that runs PowerSync in the render process instead of in the main one.

demos/example-electron-node/forge.config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ import { MakerDeb } from '@electron-forge/maker-deb';
99
import { MakerRpm } from '@electron-forge/maker-rpm';
1010
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
1111
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
12-
import type { Configuration, ModuleOptions } from 'webpack';
12+
import { type Configuration, type ModuleOptions, type DefinePlugin } from 'webpack';
13+
import * as dotenv from 'dotenv';
1314
import type IForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
1415
import type ICopyPlugin from 'copy-webpack-plugin';
1516

17+
dotenv.config({path: '.env.local'});
18+
1619
const require = createRequire(import.meta.url);
1720

1821
const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
1922
const CopyPlugin: typeof ICopyPlugin = require('copy-webpack-plugin');
23+
const DefinePluginImpl: typeof DefinePlugin = require('webpack').DefinePlugin;
2024

2125
const webpackPlugins = [
2226
new ForkTsCheckerWebpackPlugin({
@@ -86,6 +90,10 @@ const mainConfig: Configuration = {
8690
to: extensionPath,
8791
}],
8892
}),
93+
new DefinePluginImpl({
94+
POWERSYNC_URL: JSON.stringify(process.env.POWERSYNC_URL),
95+
POWERSYNC_TOKEN: JSON.stringify(process.env.POWERSYNC_TOKEN),
96+
}),
8997
],
9098
resolve: {
9199
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json']

demos/example-electron-node/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@vercel/webpack-asset-relocator-loader": "1.7.3",
2727
"copy-webpack-plugin": "^13.0.0",
2828
"css-loader": "^6.11.0",
29+
"dotenv": "^16.4.7",
2930
"electron": "30.0.2",
3031
"electron-rebuild": "^3.2.9",
3132
"fork-ts-checker-webpack-plugin": "^9.0.2",

demos/example-electron-node/src/main/index.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { Worker } from 'node:worker_threads';
22

3-
import { PowerSyncDatabase } from '@powersync/node';
4-
import { app, BrowserWindow, ipcMain } from 'electron';
5-
import { AppSchema } from './powersync';
3+
import { PowerSyncDatabase, SyncStreamConnectionMethod } from '@powersync/node';
4+
import { app, BrowserWindow, ipcMain, MessagePortMain } from 'electron';
5+
import { AppSchema, BackendConnector } from './powersync';
66
import { default as Logger } from 'js-logger';
77

88
const logger = Logger.get('PowerSyncDemo');
99
Logger.useDefaults({ defaultLevel: logger.WARN });
1010

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+
1117
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
1218
if (require('electron-squirrel-startup')) {
1319
app.quit();
@@ -24,12 +30,6 @@ const database = new PowerSyncDatabase({
2430
logger
2531
});
2632

27-
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
28-
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
29-
// whether you're running in development or production).
30-
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
31-
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
32-
3333
const createWindow = (): void => {
3434
// Create the browser window.
3535
const mainWindow = new BrowserWindow({
@@ -51,6 +51,50 @@ const createWindow = (): void => {
5151
// initialization and is ready to create browser windows.
5252
// Some APIs can only be used after this event occurs.
5353
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+
5498
ipcMain.handle('get', async (_, sql: string, args: any[]) => {
5599
return await database.get(sql, args);
56100
});

demos/example-electron-node/src/main/powersync.ts

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,66 @@
11
import { AbstractPowerSyncDatabase, column, PowerSyncBackendConnector, Schema, Table } from '@powersync/node';
22

3-
export class DemoConnector implements PowerSyncBackendConnector {
3+
declare const POWERSYNC_URL: string|null;
4+
declare const POWERSYNC_TOKEN: string|null;
5+
6+
export class BackendConnector implements PowerSyncBackendConnector {
7+
private powersyncUrl: string | undefined;
8+
private powersyncToken: string | undefined;
9+
10+
constructor() {
11+
this.powersyncUrl = POWERSYNC_URL;
12+
// This token is for development only.
13+
// For production applications, integrate with an auth provider or custom auth.
14+
this.powersyncToken = POWERSYNC_TOKEN;
15+
}
16+
417
async fetchCredentials() {
5-
const response = await fetch(`${process.env.BACKEND}/api/auth/token`);
6-
if (response.status != 200) {
7-
throw 'Could not fetch token';
18+
// TODO: Use an authentication service or custom implementation here.
19+
if (this.powersyncToken == null || this.powersyncUrl == null) {
20+
return null;
821
}
922

10-
const { token } = await response.json();
11-
1223
return {
13-
endpoint: process.env.SYNC_SERVICE!,
14-
token: token
24+
endpoint: this.powersyncUrl,
25+
token: this.powersyncToken
1526
};
1627
}
1728

18-
async uploadData(database: AbstractPowerSyncDatabase) {
19-
const batch = await database.getCrudBatch();
20-
if (batch == null) {
29+
async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {
30+
const transaction = await database.getNextCrudTransaction();
31+
32+
if (!transaction) {
2133
return;
2234
}
2335

24-
const entries: any[] = [];
25-
for (const op of batch.crud) {
26-
entries.push({
27-
table: op.table,
28-
op: op.op,
29-
id: op.id,
30-
data: op.opData
31-
});
32-
}
36+
try {
37+
// TODO: Upload here
3338

34-
const response = await fetch(`${process.env.BACKEND}/api/data/`, {
35-
method: 'POST',
36-
headers: { 'Content-Type': 'application/json' },
37-
body: JSON.stringify({ batch: entries })
38-
});
39-
if (response.status !== 200) {
40-
throw new Error(`Server returned HTTP ${response.status}: ${await response.text()}`);
39+
await transaction.complete();
40+
} catch (error: any) {
41+
if (shouldDiscardDataOnError(error)) {
42+
// Instead of blocking the queue with these errors, discard the (rest of the) transaction.
43+
//
44+
// Note that these errors typically indicate a bug in the application.
45+
// If protecting against data loss is important, save the failing records
46+
// elsewhere instead of discarding, and/or notify the user.
47+
console.error(`Data upload error - discarding`, error);
48+
await transaction.complete();
49+
} else {
50+
// Error may be retryable - e.g. network error or temporary server error.
51+
// Throwing an error here causes this call to be retried after a delay.
52+
throw error;
53+
}
4154
}
42-
43-
await batch?.complete();
4455
}
4556
}
4657

58+
function shouldDiscardDataOnError(error: any) {
59+
// TODO: Ignore non-retryable errors here
60+
return false;
61+
}
62+
63+
4764
export const LIST_TABLE = 'lists';
4865
export const TODO_TABLE = 'todos';
4966

demos/example-electron-node/src/render/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,12 @@ <h1>PowerSync demo</h1>
1212
enter <code>await powersync.get('SELECT powersync_rs_version()');</code>
1313
in the DevTools console.
1414
</p>
15+
16+
<p>SyncStatus: <code id="sync-status"></code></p>
17+
<p>
18+
Results of running <code>SELECT * FROM lists</code> (updates automatically).
19+
20+
<li id="results"></li>
21+
</p>
1522
</body>
1623
</html>
Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,31 @@
1-
console.log('👋 This message is being logged by "renderer.js", included via webpack');
1+
import { type SyncStatus } from "@powersync/node";
2+
3+
// Declared in preload.ts
4+
declare const powersync: {
5+
addPort: (port: MessagePort) => void,
6+
get: (sql: string, variables: any[]) => Promise<any[]>,
7+
syncStatus: (cb: (status: SyncStatus) => void) => void,
8+
watch: (sql: string, args: any[], cb: (rows: any[]) => void) => void,
9+
};
10+
11+
const syncStatusTarget = document.getElementById('sync-status')!;
12+
const results = document.getElementById('results') as HTMLLIElement;
13+
14+
// Simple query
15+
powersync.get('SELECT powersync_rs_version()', []).then(console.log);
16+
17+
powersync.syncStatus((status) => {
18+
syncStatusTarget.innerText = `${JSON.stringify(status)}`;
19+
});
20+
21+
powersync.watch('SELECT * FROM lists', [], (rows) => {
22+
console.log(rows);
23+
const newElements: HTMLUListElement[] = [];
24+
for (const entry of rows) {
25+
const dom = document.createElement('ul');
26+
dom.textContent = JSON.stringify(entry);
27+
newElements.push(dom);
28+
}
29+
30+
results.replaceChildren(...newElements);
31+
});
Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
1+
import { type SyncStatus } from '@powersync/node';
12
import { contextBridge, ipcRenderer } from 'electron';
23

34
contextBridge.exposeInMainWorld('powersync', {
4-
get: (sql: string, variables: any[]) => ipcRenderer.invoke('get', sql, variables)
5+
get: (sql: string, variables: any[]) => ipcRenderer.invoke('get', sql, variables),
6+
getAll: (sql: string, variables: any[]) => ipcRenderer.invoke('get', sql, variables),
7+
syncStatus: (cb: (status: SyncStatus) => void) => {
8+
const channel = new MessageChannel();
9+
channel.port1.onmessage = (event) => cb(event.data);
10+
11+
ipcRenderer.postMessage('port', null, [channel.port2]);
12+
channel.port1.postMessage({method: 'syncStatus'});
13+
},
14+
watch: (sql: string, args: any[], cb: (rows: any[]) => void) => {
15+
const channel = new MessageChannel();
16+
channel.port1.onmessage = (event) => cb(event.data);
17+
18+
const message = {method: 'watch', 'payload': {sql, args}};
19+
ipcRenderer.postMessage('port', null, [channel.port2]);
20+
channel.port1.postMessage(message);
21+
22+
return () => channel.port1.close(); // Closign the port also closes resources on the main process
23+
},
524
});

0 commit comments

Comments
 (0)