Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hip-lamps-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/node': minor
---

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`.
5 changes: 5 additions & 0 deletions .changeset/thick-lies-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/common': minor
---

Added `fetchOptions` to AbstractRemoteOptions. Allows consumers to include fields such as `dispatcher` (e.g. for proxy support) to the fetch invocations.
4 changes: 2 additions & 2 deletions demos/example-node/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import repl_factory from 'node:repl';
import { once } from 'node:events';
import repl_factory from 'node:repl';

import { PowerSyncDatabase, SyncStreamConnectionMethod } from '@powersync/node';
import { default as Logger } from 'js-logger';
import { AppSchema, DemoConnector } from './powersync.js';
import { exit } from 'node:process';
import { AppSchema, DemoConnector } from './powersync.js';

const main = async () => {
const logger = Logger.get('PowerSyncDemo');
Expand Down
19 changes: 17 additions & 2 deletions packages/common/src/client/sync/stream/AbstractRemote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,23 @@ export type AbstractRemoteOptions = {
* Binding should be done before passing here.
*/
fetchImplementation: FetchImplementation | FetchImplementationProvider;

/**
* Optional options to pass directly to all `fetch` calls.
*
* This can include fields such as `dispatcher` (e.g. for proxy support),
* `cache`, or any other fetch-compatible options.
*/
fetchOptions?: {};
};

export const DEFAULT_REMOTE_OPTIONS: AbstractRemoteOptions = {
socketUrlTransformer: (url) =>
url.replace(/^https?:\/\//, function (match) {
return match === 'https://' ? 'wss://' : 'ws://';
}),
fetchImplementation: new FetchImplementationProvider()
fetchImplementation: new FetchImplementationProvider(),
fetchOptions: {}
};

export abstract class AbstractRemote {
Expand Down Expand Up @@ -231,6 +240,10 @@ export abstract class AbstractRemote {
*/
abstract getBSON(): Promise<BSONImplementation>;

protected createSocket(url: string): WebSocket {
return new WebSocket(url);
}

/**
* Connects to the sync/stream websocket endpoint
*/
Expand All @@ -249,7 +262,8 @@ export abstract class AbstractRemote {

const connector = new RSocketConnector({
transport: new WebsocketClientTransport({
url: this.options.socketUrlTransformer(request.url)
url: this.options.socketUrlTransformer(request.url),
wsCreator: (url) => this.createSocket(url)
}),
setup: {
keepAlive: KEEP_ALIVE_MS,
Expand Down Expand Up @@ -421,6 +435,7 @@ export abstract class AbstractRemote {
body: JSON.stringify(data),
signal: controller.signal,
cache: 'no-store',
...(this.options.fetchOptions ?? {}),
...options.fetchOptions
}).catch((ex) => {
if (ex.name == 'AbortError') {
Expand Down
16 changes: 16 additions & 0 deletions packages/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ contains everything you need to know to get started implementing PowerSync in yo

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

# Proxy Support

This SDK supports HTTP, HTTPS, and WebSocket proxies via environment variables.

## HTTP Connection Method

Internally we probe the http environment variables and apply it to fetch requests ([undici](https://www.npmjs.com/package/undici/v/5.6.0))

- Set the `HTTPS_PROXY` or `HTTP_PROXY` environment variable to automatically route HTTP requests through a proxy.

## WEB Socket Connection Method

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:

- Set the `WS_PROXY` or `WSS_PROXY` environment variable to route the webocket connections through a proxy.

# Found a bug or need help?

- Join our [Discord server](https://discord.gg/powersync) where you can browse topics from our community, ask questions, share feedback, or just say hello :)
Expand Down
7 changes: 5 additions & 2 deletions packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,15 @@
"@powersync/common": "workspace:*",
"async-lock": "^1.4.0",
"bson": "^6.6.0",
"comlink": "^4.4.2"
"comlink": "^4.4.2",
"proxy-agent": "^6.5.0",
"undici": "^7.8.0",
"ws": "^8.18.1"
},
"devDependencies": {
"@powersync/drizzle-driver": "workspace:*",
"@types/async-lock": "^1.4.0",
"drizzle-orm": "^0.35.2",
"@powersync/drizzle-driver": "workspace:*",
"rollup": "4.14.3",
"typescript": "^5.5.3",
"vitest": "^3.0.5"
Expand Down
28 changes: 27 additions & 1 deletion packages/node/src/sync/stream/NodeRemote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
RemoteConnector
} from '@powersync/common';
import { BSON } from 'bson';
import Agent from 'proxy-agent';
import { ProxyAgent } from 'undici';
import { WebSocket } from 'ws';

export const STREAMING_POST_TIMEOUT_MS = 30_000;

Expand All @@ -22,15 +25,38 @@ class NodeFetchProvider extends FetchImplementationProvider {
}

export class NodeRemote extends AbstractRemote {
protected agent: ProxyAgent | undefined;

constructor(
protected connector: RemoteConnector,
protected logger: ILogger = DEFAULT_REMOTE_LOGGER,
options?: Partial<AbstractRemoteOptions>
) {
// Automatic env vars are not supported by undici
// proxy-agent does not work directly with dispatcher
const proxy = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY;
const agent = proxy ? new ProxyAgent(proxy) : undefined;

super(connector, logger, {
...(options ?? {}),
fetchImplementation: options?.fetchImplementation ?? new NodeFetchProvider()
fetchImplementation: options?.fetchImplementation ?? new NodeFetchProvider(),
fetchOptions: {
dispatcher: agent
}
});

this.agent = agent;
}

protected createSocket(url: string): globalThis.WebSocket {
return new WebSocket(url, {
// Undici does not seem to be compatible with ws, using `proxy-agent` instead.
// Automatically uses WS_PROXY or WSS_PROXY env vars
agent: new Agent.ProxyAgent(),
headers: {
'User-Agent': this.getUserAgent()
}
}) as any as globalThis.WebSocket; // This is compatible in Node environments
}

getUserAgent(): string {
Expand Down
Loading