Skip to content

Commit 12c52c1

Browse files
committed
Merge branch 'main' into feature/logging-cleanup-demos
2 parents 88a7dd0 + 6807df6 commit 12c52c1

File tree

10 files changed

+144
-74
lines changed

10 files changed

+144
-74
lines changed

.changeset/hip-lamps-draw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/node': minor
3+
---
4+
5+
Introduced support for specifying proxy environment variables for the connection methods. For HTTP it supports `HTTP_PROXY` or `HTTPS_PROXY`, and for WebSockets it supports `WS_PROXY` and `WSS_PROXY`.

.changeset/thick-lies-invent.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@powersync/common': minor
3+
---
4+
5+
Added `fetchOptions` to AbstractRemoteOptions. Allows consumers to include fields such as `dispatcher` (e.g. for proxy support) to the fetch invocations.
6+
Also ensuring all options provided to `connect()` are passed onwards, allows packages to have their own option definitions for `connect()` and the abstract `generateSyncStreamImplementation()`.

demos/example-node/src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import repl_factory from 'node:repl';
21
import { once } from 'node:events';
2+
import repl_factory from 'node:repl';
33

44
import { createBaseLogger, createLogger, PowerSyncDatabase, SyncStreamConnectionMethod } from '@powersync/node';
5-
import { AppSchema, DemoConnector } from './powersync.js';
65
import { exit } from 'node:process';
6+
import { AppSchema, DemoConnector } from './powersync.js';
77

88
const main = async () => {
99
const baseLogger = createBaseLogger();

packages/common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
421421
// Use the options passed in during connect, or fallback to the options set during database creation or fallback to the default options
422422
resolvedConnectionOptions(options?: PowerSyncConnectionOptions): RequiredAdditionalConnectionOptions {
423423
return {
424+
...options,
424425
retryDelayMs:
425426
options?.retryDelayMs ?? this.options.retryDelayMs ?? this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS,
426427
crudUploadThrottleMs:
@@ -440,12 +441,9 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
440441
throw new Error('Cannot connect using a closed client');
441442
}
442443

443-
const { retryDelayMs, crudUploadThrottleMs } = this.resolvedConnectionOptions(options);
444+
const resolvedConnectOptions = this.resolvedConnectionOptions(options);
444445

445-
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector, {
446-
retryDelayMs,
447-
crudUploadThrottleMs
448-
});
446+
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector, resolvedConnectOptions);
449447
this.syncStatusListenerDisposer = this.syncStreamImplementation.registerListener({
450448
statusChanged: (status) => {
451449
this.currentStatus = new SyncStatus({

packages/common/src/client/sync/stream/AbstractRemote.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,23 @@ export type AbstractRemoteOptions = {
8585
* Binding should be done before passing here.
8686
*/
8787
fetchImplementation: FetchImplementation | FetchImplementationProvider;
88+
89+
/**
90+
* Optional options to pass directly to all `fetch` calls.
91+
*
92+
* This can include fields such as `dispatcher` (e.g. for proxy support),
93+
* `cache`, or any other fetch-compatible options.
94+
*/
95+
fetchOptions?: {};
8896
};
8997

9098
export const DEFAULT_REMOTE_OPTIONS: AbstractRemoteOptions = {
9199
socketUrlTransformer: (url) =>
92100
url.replace(/^https?:\/\//, function (match) {
93101
return match === 'https://' ? 'wss://' : 'ws://';
94102
}),
95-
fetchImplementation: new FetchImplementationProvider()
103+
fetchImplementation: new FetchImplementationProvider(),
104+
fetchOptions: {}
96105
};
97106

98107
export abstract class AbstractRemote {
@@ -231,6 +240,10 @@ export abstract class AbstractRemote {
231240
*/
232241
abstract getBSON(): Promise<BSONImplementation>;
233242

243+
protected createSocket(url: string): WebSocket {
244+
return new WebSocket(url);
245+
}
246+
234247
/**
235248
* Connects to the sync/stream websocket endpoint
236249
*/
@@ -249,7 +262,8 @@ export abstract class AbstractRemote {
249262

250263
const connector = new RSocketConnector({
251264
transport: new WebsocketClientTransport({
252-
url: this.options.socketUrlTransformer(request.url)
265+
url: this.options.socketUrlTransformer(request.url),
266+
wsCreator: (url) => this.createSocket(url)
253267
}),
254268
setup: {
255269
keepAlive: KEEP_ALIVE_MS,
@@ -421,6 +435,7 @@ export abstract class AbstractRemote {
421435
body: JSON.stringify(data),
422436
signal: controller.signal,
423437
cache: 'no-store',
438+
...(this.options.fetchOptions ?? {}),
424439
...options.fetchOptions
425440
}).catch((ex) => {
426441
if (ex.name == 'AbortError') {

packages/node/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ contains everything you need to know to get started implementing PowerSync in yo
5656

5757
A simple example using `@powersync/node` is available in the [`demos/example-node/`](../demos/example-node) directory.
5858

59+
# Proxy Support
60+
61+
This SDK supports HTTP, HTTPS, and WebSocket proxies via environment variables.
62+
63+
## HTTP Connection Method
64+
65+
Internally we probe the http environment variables and apply it to fetch requests ([undici](https://www.npmjs.com/package/undici/v/5.6.0))
66+
67+
- Set the `HTTPS_PROXY` or `HTTP_PROXY` environment variable to automatically route HTTP requests through a proxy.
68+
69+
## WEB Socket Connection Method
70+
71+
Internally the [proxy-agent](https://www.npmjs.com/package/proxy-agent) dependency for WebSocket proxies, which has its own internal code for automatically picking up the appropriate environment variables:
72+
73+
- Set the `WS_PROXY` or `WSS_PROXY` environment variable to route the webocket connections through a proxy.
74+
5975
# Found a bug or need help?
6076

6177
- Join our [Discord server](https://discord.gg/powersync) where you can browse topics from our community, ask questions, share feedback, or just say hello :)

packages/node/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,15 @@
5151
"@powersync/common": "workspace:*",
5252
"async-lock": "^1.4.0",
5353
"bson": "^6.6.0",
54-
"comlink": "^4.4.2"
54+
"comlink": "^4.4.2",
55+
"proxy-agent": "^6.5.0",
56+
"undici": "^7.8.0",
57+
"ws": "^8.18.1"
5558
},
5659
"devDependencies": {
60+
"@powersync/drizzle-driver": "workspace:*",
5761
"@types/async-lock": "^1.4.0",
5862
"drizzle-orm": "^0.35.2",
59-
"@powersync/drizzle-driver": "workspace:*",
6063
"rollup": "4.14.3",
6164
"typescript": "^5.5.3",
6265
"vitest": "^3.0.5"

packages/node/src/db/PowerSyncDatabase.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import {
22
AbstractPowerSyncDatabase,
33
AbstractStreamingSyncImplementation,
4+
AdditionalConnectionOptions,
45
BucketStorageAdapter,
56
DBAdapter,
67
PowerSyncBackendConnector,
8+
PowerSyncConnectionOptions,
79
PowerSyncDatabaseOptions,
810
PowerSyncDatabaseOptionsWithSettings,
11+
RequiredAdditionalConnectionOptions,
912
SqliteBucketStorage,
1013
SQLOpenFactory
1114
} from '@powersync/common';
@@ -15,11 +18,22 @@ import { NodeStreamingSyncImplementation } from '../sync/stream/NodeStreamingSyn
1518

1619
import { BetterSQLite3DBAdapter } from './BetterSQLite3DBAdapter.js';
1720
import { NodeSQLOpenOptions } from './options.js';
21+
import { Dispatcher } from 'undici';
1822

1923
export type NodePowerSyncDatabaseOptions = PowerSyncDatabaseOptions & {
2024
database: DBAdapter | SQLOpenFactory | NodeSQLOpenOptions;
2125
};
2226

27+
export type NodeAdditionalConnectionOptions = AdditionalConnectionOptions & {
28+
/**
29+
* Optional custom dispatcher for HTTP connections (e.g. using undici).
30+
* Only used when the connection method is SyncStreamConnectionMethod.HTTP
31+
*/
32+
dispatcher?: Dispatcher;
33+
};
34+
35+
export type NodePowerSyncConnectionOptions = PowerSyncConnectionOptions & NodeAdditionalConnectionOptions;
36+
2337
/**
2438
* A PowerSync database which provides SQLite functionality
2539
* which is automatically synced.
@@ -54,10 +68,18 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
5468
return new SqliteBucketStorage(this.database, AbstractPowerSyncDatabase.transactionMutex);
5569
}
5670

71+
connect(
72+
connector: PowerSyncBackendConnector,
73+
options?: PowerSyncConnectionOptions & { dispatcher?: Dispatcher }
74+
): Promise<void> {
75+
return super.connect(connector, options);
76+
}
77+
5778
protected generateSyncStreamImplementation(
58-
connector: PowerSyncBackendConnector
79+
connector: PowerSyncBackendConnector,
80+
options: NodeAdditionalConnectionOptions
5981
): AbstractStreamingSyncImplementation {
60-
const remote = new NodeRemote(connector);
82+
const remote = new NodeRemote(connector, this.options.logger, { dispatcher: options.dispatcher });
6183

6284
return new NodeStreamingSyncImplementation({
6385
adapter: this.bucketStorageAdapter,

packages/node/src/sync/stream/NodeRemote.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
RemoteConnector
1212
} from '@powersync/common';
1313
import { BSON } from 'bson';
14+
import Agent from 'proxy-agent';
15+
import { EnvHttpProxyAgent, Dispatcher } from 'undici';
16+
import { WebSocket } from 'ws';
1417

1518
export const STREAMING_POST_TIMEOUT_MS = 30_000;
1619

@@ -20,18 +23,38 @@ class NodeFetchProvider extends FetchImplementationProvider {
2023
}
2124
}
2225

26+
export type NodeRemoteOptions = AbstractRemoteOptions & {
27+
dispatcher?: Dispatcher;
28+
};
29+
2330
export class NodeRemote extends AbstractRemote {
2431
constructor(
2532
protected connector: RemoteConnector,
2633
protected logger: ILogger = DEFAULT_REMOTE_LOGGER,
27-
options?: Partial<AbstractRemoteOptions>
34+
options?: Partial<NodeRemoteOptions>
2835
) {
36+
// EnvHttpProxyAgent automatically uses relevant env vars for HTTP
37+
const dispatcher = options?.dispatcher ?? new EnvHttpProxyAgent();
38+
2939
super(connector, logger, {
3040
...(options ?? {}),
31-
fetchImplementation: options?.fetchImplementation ?? new NodeFetchProvider()
41+
fetchImplementation: options?.fetchImplementation ?? new NodeFetchProvider(),
42+
fetchOptions: {
43+
dispatcher
44+
}
3245
});
3346
}
3447

48+
protected createSocket(url: string): globalThis.WebSocket {
49+
return new WebSocket(url, {
50+
// Automatically uses relevant env vars for web sockets
51+
agent: new Agent.ProxyAgent(),
52+
headers: {
53+
'User-Agent': this.getUserAgent()
54+
}
55+
}) as any as globalThis.WebSocket; // This is compatible in Node environments
56+
}
57+
3558
getUserAgent(): string {
3659
return [
3760
super.getUserAgent(),

0 commit comments

Comments
 (0)