Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions .changeset/quiet-owls-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@powersync/react-native': minor
'@powersync/common': minor
'@powersync/node': minor
'@powersync/web': minor
'@powersync/capacitor': minor
---

Added ability to specify `appMetadata` for sync/stream requests.

Note: This requires a PowerSync service version `>=1.17.0` in order for logs to display metadata.

```javascript
powerSync.connect(connector, {
// This will be included in PowerSync service logs
appMetadata: {
app_version: MY_APP_VERSION
}
});
```
9 changes: 5 additions & 4 deletions demos/example-node/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import {
SyncStreamConnectionMethod
} from '@powersync/node';
import { exit } from 'node:process';
import { WorkerOpener } from 'node_modules/@powersync/node/src/db/options.js';
import { AppSchema, DemoConnector } from './powersync.js';
import { enableUncidiDiagnostics } from './UndiciDiagnostics.js';
import { WorkerOpener } from 'node_modules/@powersync/node/src/db/options.js';
import { LockContext } from 'node_modules/@powersync/node/dist/bundle.cjs';

const main = async () => {
const baseLogger = createBaseLogger();
Expand Down Expand Up @@ -59,10 +58,12 @@ const main = async () => {
logger
});
console.log(await db.get('SELECT powersync_rs_version();'));

await db.connect(new DemoConnector(), {
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
clientImplementation: SyncClientImplementation.RUST
clientImplementation: SyncClientImplementation.RUST,
appMetadata: {
app_version: process.env.npm_package_version || 'unknown'
}
});
// Example using a proxy agent for more control over the connection:
// const proxyAgent = new (await import('undici')).ProxyAgent({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { createBaseLogger, DifferentialWatchedQuery, LogLevel, PowerSyncDatabase
import React, { Suspense } from 'react';
import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext';

declare const APP_VERSION: string;

const SupabaseContext = React.createContext<SupabaseConnector | null>(null);
export const useSupabase = () => React.useContext(SupabaseContext);

Expand Down Expand Up @@ -68,7 +70,11 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => {
const l = connector.registerListener({
initialized: () => {},
sessionStarted: () => {
powerSync.connect(connector);
powerSync.connect(connector, {
appMetadata: {
app_version: APP_VERSION
}
});
}
});

Expand Down
9 changes: 6 additions & 3 deletions demos/react-supabase-todolist/vite.config.mts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';
import { fileURLToPath, URL } from 'url';
import topLevelAwait from 'vite-plugin-top-level-await';
import wasm from 'vite-plugin-wasm';

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';

// https://vitejs.dev/config/
Expand All @@ -19,6 +19,9 @@ export default defineConfig({
resolve: {
alias: [{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }]
},
define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version)
},
publicDir: '../public',
envDir: '..', // Use this dir for env vars, not 'src'.
optimizeDeps: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { CrudEntry } from '../bucket/CrudEntry.js';
import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
import { AbstractRemote, FetchStrategy, SyncStreamOptions } from './AbstractRemote.js';
import { coreStatusToJs, EstablishSyncStream, Instruction, SyncPriorityStatus } from './core-instruction.js';
import { EstablishSyncStream, Instruction, coreStatusToJs } from './core-instruction.js';
import {
BucketRequest,
CrudUploadNotification,
Expand Down Expand Up @@ -129,6 +129,11 @@ export interface InternalConnectionOptions extends BaseConnectionOptions, Additi

/** @internal */
export interface BaseConnectionOptions {
/**
* A set of metadata to be included in service logs.
*/
appMetadata?: Record<string, string>;

/**
* Whether to use a JavaScript implementation to handle received sync lines from the sync
* service, or whether this work should be offloaded to the PowerSync core extension.
Expand Down Expand Up @@ -223,6 +228,7 @@ export const DEFAULT_STREAMING_SYNC_OPTIONS = {
export type RequiredPowerSyncConnectionOptions = Required<BaseConnectionOptions>;

export const DEFAULT_STREAM_CONNECTION_OPTIONS: RequiredPowerSyncConnectionOptions = {
appMetadata: {},
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
clientImplementation: DEFAULT_SYNC_CLIENT_IMPLEMENTATION,
fetchStrategy: FetchStrategy.Buffered,
Expand Down Expand Up @@ -658,6 +664,16 @@ The next upload iteration will be delayed.`);
...DEFAULT_STREAM_CONNECTION_OPTIONS,
...(options ?? {})
};

// Validate app metadata
const invalidMetadata = Object.entries(resolvedOptions.appMetadata).filter(
([_, value]) => typeof value != 'string'
);
if (invalidMetadata.length > 0) {
throw new Error(
`Invalid appMetadata provided. Only string values are allowed. Invalid values: ${invalidMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')}`
);
}
const clientImplementation = resolvedOptions.clientImplementation;
this.updateSyncStatus({ clientImplementation });

Expand Down Expand Up @@ -699,6 +715,7 @@ The next upload iteration will be delayed.`);
include_checksum: true,
raw_data: true,
parameters: resolvedOptions.params,
app_metadata: resolvedOptions.appMetadata,
client_id: clientId
}
};
Expand Down Expand Up @@ -1088,6 +1105,7 @@ The next upload iteration will be delayed.`);
try {
const options: any = {
parameters: resolvedOptions.params,
app_metadata: resolvedOptions.appMetadata,
active_streams: this.activeStreams,
include_defaults: resolvedOptions.includeDefaultStreams
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ export interface StreamingSyncRequest {
*/
parameters?: Record<string, StreamingSyncRequestParameterType>;

/**
* Application metadata to be included in service logs.
*/
app_metadata?: Record<string, string>;

client_id?: string;
}

Expand Down
29 changes: 29 additions & 0 deletions packages/node/tests/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,35 @@ function defineSyncTests(impl: SyncClientImplementation) {
expect.arrayContaining([expect.stringContaining('Cannot enqueue data into closed stream')])
);
});

mockSyncServiceTest('passes app metadata to the sync service', async ({ syncService }) => {
const database = await syncService.createDatabase();
let connectCompleted = false;
database
.connect(new TestConnector(), {
...options,
appMetadata: {
name: 'test'
}
})
.then(() => {
connectCompleted = true;
});
expect(connectCompleted).toBeFalsy();

await vi.waitFor(() => expect(syncService.connectedListeners).toHaveLength(1));
// We want connected: true once we have a connection

await vi.waitFor(() => connectCompleted);
// The request should contain the app metadata
expect(syncService.connectedListeners[0]).toMatchObject(
expect.objectContaining({
app_metadata: {
name: 'test'
}
})
);
});
}

async function waitForProgress(
Expand Down