Skip to content

Commit 616c2a1

Browse files
feat: Custom App Metadata (#783)
1 parent 299c6dc commit 616c2a1

File tree

7 files changed

+91
-9
lines changed

7 files changed

+91
-9
lines changed

.changeset/quiet-owls-flash.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@powersync/react-native': minor
3+
'@powersync/common': minor
4+
'@powersync/node': minor
5+
'@powersync/web': minor
6+
'@powersync/capacitor': minor
7+
---
8+
9+
Added ability to specify `appMetadata` for sync/stream requests.
10+
11+
Note: This requires a PowerSync service version `>=1.17.0` in order for logs to display metadata.
12+
13+
```javascript
14+
powerSync.connect(connector, {
15+
// This will be included in PowerSync service logs
16+
appMetadata: {
17+
app_version: MY_APP_VERSION
18+
}
19+
});
20+
```

demos/example-node/src/main.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import {
1010
SyncStreamConnectionMethod
1111
} from '@powersync/node';
1212
import { exit } from 'node:process';
13+
import { WorkerOpener } from 'node_modules/@powersync/node/src/db/options.js';
1314
import { AppSchema, DemoConnector } from './powersync.js';
1415
import { enableUncidiDiagnostics } from './UndiciDiagnostics.js';
15-
import { WorkerOpener } from 'node_modules/@powersync/node/src/db/options.js';
16-
import { LockContext } from 'node_modules/@powersync/node/dist/bundle.cjs';
1716

1817
const main = async () => {
1918
const baseLogger = createBaseLogger();
@@ -59,10 +58,12 @@ const main = async () => {
5958
logger
6059
});
6160
console.log(await db.get('SELECT powersync_rs_version();'));
62-
6361
await db.connect(new DemoConnector(), {
6462
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
65-
clientImplementation: SyncClientImplementation.RUST
63+
clientImplementation: SyncClientImplementation.RUST,
64+
appMetadata: {
65+
app_version: process.env.npm_package_version || 'unknown'
66+
}
6667
});
6768
// Example using a proxy agent for more control over the connection:
6869
// const proxyAgent = new (await import('undici')).ProxyAgent({

demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { createBaseLogger, DifferentialWatchedQuery, LogLevel, PowerSyncDatabase
77
import React, { Suspense } from 'react';
88
import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext';
99

10+
declare const APP_VERSION: string;
11+
1012
const SupabaseContext = React.createContext<SupabaseConnector | null>(null);
1113
export const useSupabase = () => React.useContext(SupabaseContext);
1214

@@ -68,7 +70,11 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => {
6870
const l = connector.registerListener({
6971
initialized: () => {},
7072
sessionStarted: () => {
71-
powerSync.connect(connector);
73+
powerSync.connect(connector, {
74+
appMetadata: {
75+
app_version: APP_VERSION
76+
}
77+
});
7278
}
7379
});
7480

demos/react-supabase-todolist/vite.config.mts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import wasm from 'vite-plugin-wasm';
2-
import topLevelAwait from 'vite-plugin-top-level-await';
31
import { fileURLToPath, URL } from 'url';
2+
import topLevelAwait from 'vite-plugin-top-level-await';
3+
import wasm from 'vite-plugin-wasm';
44

5-
import { defineConfig } from 'vite';
65
import react from '@vitejs/plugin-react';
6+
import { defineConfig } from 'vite';
77
import { VitePWA } from 'vite-plugin-pwa';
88

99
// https://vitejs.dev/config/
@@ -19,6 +19,9 @@ export default defineConfig({
1919
resolve: {
2020
alias: [{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }]
2121
},
22+
define: {
23+
APP_VERSION: JSON.stringify(process.env.npm_package_version)
24+
},
2225
publicDir: '../public',
2326
envDir: '..', // Use this dir for env vars, not 'src'.
2427
optimizeDeps: {

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import { CrudEntry } from '../bucket/CrudEntry.js';
1717
import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
1818
import { AbstractRemote, FetchStrategy, SyncStreamOptions } from './AbstractRemote.js';
19-
import { coreStatusToJs, EstablishSyncStream, Instruction, SyncPriorityStatus } from './core-instruction.js';
19+
import { EstablishSyncStream, Instruction, coreStatusToJs } from './core-instruction.js';
2020
import {
2121
BucketRequest,
2222
CrudUploadNotification,
@@ -129,6 +129,11 @@ export interface InternalConnectionOptions extends BaseConnectionOptions, Additi
129129

130130
/** @internal */
131131
export interface BaseConnectionOptions {
132+
/**
133+
* A set of metadata to be included in service logs.
134+
*/
135+
appMetadata?: Record<string, string>;
136+
132137
/**
133138
* Whether to use a JavaScript implementation to handle received sync lines from the sync
134139
* service, or whether this work should be offloaded to the PowerSync core extension.
@@ -223,6 +228,7 @@ export const DEFAULT_STREAMING_SYNC_OPTIONS = {
223228
export type RequiredPowerSyncConnectionOptions = Required<BaseConnectionOptions>;
224229

225230
export const DEFAULT_STREAM_CONNECTION_OPTIONS: RequiredPowerSyncConnectionOptions = {
231+
appMetadata: {},
226232
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
227233
clientImplementation: DEFAULT_SYNC_CLIENT_IMPLEMENTATION,
228234
fetchStrategy: FetchStrategy.Buffered,
@@ -658,6 +664,16 @@ The next upload iteration will be delayed.`);
658664
...DEFAULT_STREAM_CONNECTION_OPTIONS,
659665
...(options ?? {})
660666
};
667+
668+
// Validate app metadata
669+
const invalidMetadata = Object.entries(resolvedOptions.appMetadata).filter(
670+
([_, value]) => typeof value != 'string'
671+
);
672+
if (invalidMetadata.length > 0) {
673+
throw new Error(
674+
`Invalid appMetadata provided. Only string values are allowed. Invalid values: ${invalidMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')}`
675+
);
676+
}
661677
const clientImplementation = resolvedOptions.clientImplementation;
662678
this.updateSyncStatus({ clientImplementation });
663679

@@ -699,6 +715,7 @@ The next upload iteration will be delayed.`);
699715
include_checksum: true,
700716
raw_data: true,
701717
parameters: resolvedOptions.params,
718+
app_metadata: resolvedOptions.appMetadata,
702719
client_id: clientId
703720
}
704721
};
@@ -1088,6 +1105,7 @@ The next upload iteration will be delayed.`);
10881105
try {
10891106
const options: any = {
10901107
parameters: resolvedOptions.params,
1108+
app_metadata: resolvedOptions.appMetadata,
10911109
active_streams: this.activeStreams,
10921110
include_defaults: resolvedOptions.includeDefaultStreams
10931111
};

packages/common/src/client/sync/stream/streaming-sync-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ export interface StreamingSyncRequest {
9090
*/
9191
parameters?: Record<string, StreamingSyncRequestParameterType>;
9292

93+
/**
94+
* Application metadata to be included in service logs.
95+
*/
96+
app_metadata?: Record<string, string>;
97+
9398
client_id?: string;
9499
}
95100

packages/node/tests/sync.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,35 @@ function defineSyncTests(impl: SyncClientImplementation) {
923923
expect.arrayContaining([expect.stringContaining('Cannot enqueue data into closed stream')])
924924
);
925925
});
926+
927+
mockSyncServiceTest('passes app metadata to the sync service', async ({ syncService }) => {
928+
const database = await syncService.createDatabase();
929+
let connectCompleted = false;
930+
database
931+
.connect(new TestConnector(), {
932+
...options,
933+
appMetadata: {
934+
name: 'test'
935+
}
936+
})
937+
.then(() => {
938+
connectCompleted = true;
939+
});
940+
expect(connectCompleted).toBeFalsy();
941+
942+
await vi.waitFor(() => expect(syncService.connectedListeners).toHaveLength(1));
943+
// We want connected: true once we have a connection
944+
945+
await vi.waitFor(() => connectCompleted);
946+
// The request should contain the app metadata
947+
expect(syncService.connectedListeners[0]).toMatchObject(
948+
expect.objectContaining({
949+
app_metadata: {
950+
name: 'test'
951+
}
952+
})
953+
);
954+
});
926955
}
927956

928957
async function waitForProgress(

0 commit comments

Comments
 (0)