Skip to content

Commit b363869

Browse files
committed
Diagnostics app: Support Rust client
1 parent e256286 commit b363869

File tree

10 files changed

+211
-28
lines changed

10 files changed

+211
-28
lines changed

.changeset/fuzzy-buses-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/common': minor
3+
---
4+
5+
Add `clientImplementation` field to `SyncStatus`.

.changeset/large-toes-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/diagnostics-app': patch
3+
---
4+
5+
Support Rust client.

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,8 +632,10 @@ The next upload iteration will be delayed.`);
632632
...DEFAULT_STREAM_CONNECTION_OPTIONS,
633633
...(options ?? {})
634634
};
635+
const clientImplementation = resolvedOptions.clientImplementation;
636+
this.updateSyncStatus({ clientImplementation });
635637

636-
if (resolvedOptions.clientImplementation == SyncClientImplementation.JAVASCRIPT) {
638+
if (clientImplementation == SyncClientImplementation.JAVASCRIPT) {
637639
await this.legacyStreamingSyncIteration(signal, resolvedOptions);
638640
} else {
639641
await this.requireKeyFormat(true);
@@ -1168,7 +1170,8 @@ The next upload iteration will be delayed.`);
11681170
...this.syncStatus.dataFlowStatus,
11691171
...options.dataFlow
11701172
},
1171-
priorityStatusEntries: options.priorityStatusEntries ?? this.syncStatus.priorityStatusEntries
1173+
priorityStatusEntries: options.priorityStatusEntries ?? this.syncStatus.priorityStatusEntries,
1174+
clientImplementation: options.clientImplementation ?? this.syncStatus.clientImplementation
11721175
});
11731176

11741177
if (!this.syncStatus.isEqual(updatedStatus)) {

packages/common/src/db/crud/SyncStatus.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SyncClientImplementation } from 'src/client/sync/stream/AbstractStreamingSyncImplementation.js';
12
import { InternalProgressInformation, SyncProgress } from './SyncProgress.js';
23

34
export type SyncDataFlowStatus = Partial<{
@@ -35,11 +36,22 @@ export type SyncStatusOptions = {
3536
lastSyncedAt?: Date;
3637
hasSynced?: boolean;
3738
priorityStatusEntries?: SyncPriorityStatus[];
39+
clientImplementation?: SyncClientImplementation;
3840
};
3941

4042
export class SyncStatus {
4143
constructor(protected options: SyncStatusOptions) {}
4244

45+
/**
46+
* Returns the used sync client implementation (either the one implemented in JavaScript or the newer Rust-based
47+
* implementation).
48+
*
49+
* This information is only available after a connection has been requested.
50+
*/
51+
get clientImplementation() {
52+
return this.options.clientImplementation;
53+
}
54+
4355
/**
4456
* Indicates if the client is currently connected to the PowerSync service.
4557
*

tools/diagnostics-app/CHANGELOG.md

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,5 @@
11
# diagnostics-app
22

3-
## 0.9.8
4-
5-
### Patch Changes
6-
7-
- Updated dependencies [c191989]
8-
- @powersync/react@1.7.2
9-
10-
## 0.9.7
11-
12-
### Patch Changes
13-
14-
- 47294f2: Update PowerSync core extension to version 0.4.4
15-
- Updated dependencies [c910c66]
16-
- Updated dependencies [8decd49]
17-
- Updated dependencies [9e3e3a5]
18-
- Updated dependencies [47294f2]
19-
- @powersync/web@1.26.0
20-
- @powersync/react@1.7.1
21-
223
## 0.9.6
234

245
### Patch Changes

tools/diagnostics-app/src/app/views/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export default function ViewsLayout({ children }: { children: React.ReactNode })
155155
<Box sx={{ flexGrow: 1 }}>
156156
<Typography>{title}</Typography>
157157
</Box>
158+
{syncStatus?.clientImplementation && <Typography>Client: {syncStatus?.clientImplementation}</Typography>}
158159
<NorthIcon
159160
sx={{ marginRight: '-10px' }}
160161
color={syncStatus?.dataFlowStatus.uploading ? 'primary' : 'inherit'}

tools/diagnostics-app/src/components/widgets/LoginDetailsWidget.tsx

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
import React from 'react';
2-
import { Box, Button, ButtonGroup, FormGroup, Paper, TextField, Typography, styled } from '@mui/material';
2+
import {
3+
Box,
4+
Button,
5+
ButtonGroup,
6+
FormControlLabel,
7+
FormGroup,
8+
Paper,
9+
Switch,
10+
TextField,
11+
Typography,
12+
styled
13+
} from '@mui/material';
314
import { Formik, FormikErrors } from 'formik';
15+
import { SyncClientImplementation } from '@powersync/web';
416

517
export type LoginDetailsFormValues = {
618
token: string;
719
endpoint: string;
20+
clientImplementation: SyncClientImplementation;
821
};
922

1023
export type LoginAction = {
@@ -25,7 +38,7 @@ export const LoginDetailsWidget: React.FC<LoginDetailsWidgetProps> = (props) =>
2538
<S.Logo alt="PowerSync Logo" width={400} height={100} src="/powersync-logo.svg" />
2639
</S.LogoBox>
2740
<Formik<LoginDetailsFormValues>
28-
initialValues={{ token: '', endpoint: '' }}
41+
initialValues={{ token: '', endpoint: '', clientImplementation: SyncClientImplementation.RUST }}
2942
validateOnChange={false}
3043
validateOnBlur={false}
3144
validate={(values) => {
@@ -44,15 +57,16 @@ export const LoginDetailsWidget: React.FC<LoginDetailsWidgetProps> = (props) =>
4457
}
4558
await props.onSubmit({
4659
token: values.token,
47-
endpoint
60+
endpoint,
61+
clientImplementation: values.clientImplementation
4862
});
4963
} catch (ex: any) {
5064
console.error(ex);
5165
setSubmitting(false);
5266
setFieldError('endpoint', ex.message);
5367
}
5468
}}>
55-
{({ values, errors, handleChange, handleBlur, isSubmitting, handleSubmit }) => (
69+
{({ values, errors, handleChange, handleBlur, isSubmitting, handleSubmit, setFieldValue }) => (
5670
<form onSubmit={handleSubmit}>
5771
<FormGroup>
5872
<S.TextInput
@@ -84,6 +98,34 @@ export const LoginDetailsWidget: React.FC<LoginDetailsWidgetProps> = (props) =>
8498
/>
8599
</FormGroup>
86100
<S.ActionButtonGroup>
101+
<FormControlLabel
102+
control={
103+
<Switch
104+
checked={values.clientImplementation == SyncClientImplementation.RUST}
105+
onChange={() =>
106+
setFieldValue(
107+
'clientImplementation',
108+
values.clientImplementation == SyncClientImplementation.RUST
109+
? SyncClientImplementation.JAVASCRIPT
110+
: SyncClientImplementation.RUST
111+
)
112+
}
113+
/>
114+
}
115+
label={
116+
<span>
117+
Rust sync client (
118+
<a
119+
style={{ color: 'lightblue' }}
120+
target="_blank"
121+
href="https://releases.powersync.com/announcements/improved-sync-performance-in-our-client-sdks">
122+
what's that?
123+
</a>
124+
)
125+
</span>
126+
}
127+
/>
128+
87129
<Button variant="outlined" type="submit" disabled={isSubmitting}>
88130
Proceed
89131
</Button>
@@ -143,7 +185,7 @@ namespace S {
143185
margin-top: 20px;
144186
width: 100%;
145187
display: flex;
146-
justify-content: end;
188+
justify-content: space-between;
147189
`;
148190

149191
export const TextInput = styled(TextField)`

tools/diagnostics-app/src/library/powersync/ConnectionManager.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
createBaseLogger,
44
LogLevel,
55
PowerSyncDatabase,
6+
SyncClientImplementation,
67
TemporaryStorageOption,
78
WASQLiteOpenFactory,
89
WASQLiteVFS,
@@ -14,6 +15,7 @@ import { safeParse } from '../safeParse/safeParse';
1415
import { DynamicSchemaManager } from './DynamicSchemaManager';
1516
import { RecordingStorageAdapter } from './RecordingStorageAdapter';
1617
import { TokenConnector } from './TokenConnector';
18+
import { RustClientInterceptor } from './RustClientInterceptor';
1719

1820
const baseLogger = createBaseLogger();
1921
baseLogger.useDefaults();
@@ -57,9 +59,19 @@ if (connector.hasCredentials()) {
5759
}
5860

5961
export async function connect() {
62+
const client =
63+
localStorage.getItem('preferred_client_implementation') == SyncClientImplementation.RUST
64+
? SyncClientImplementation.RUST
65+
: SyncClientImplementation.JAVASCRIPT;
66+
6067
const params = getParams();
6168
await sync?.disconnect();
6269
const remote = new WebRemote(connector);
70+
const adapter =
71+
client == SyncClientImplementation.JAVASCRIPT
72+
? new RecordingStorageAdapter(db.database, schemaManager)
73+
: new RustClientInterceptor(db.database, remote, schemaManager);
74+
6375
const syncOptions: WebStreamingSyncImplementationOptions = {
6476
adapter,
6577
remote,
@@ -69,7 +81,7 @@ export async function connect() {
6981
identifier: 'diagnostics'
7082
};
7183
sync = new WebStreamingSyncImplementation(syncOptions);
72-
await sync.connect({ params });
84+
await sync.connect({ params, clientImplementation: client });
7385
if (!sync.syncStatus.connected) {
7486
const error = sync.syncStatus.dataFlowStatus.downloadError ?? new Error('Failed to connect');
7587
// Disconnect but don't wait for it
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {
2+
AbstractPowerSyncDatabase,
3+
AbstractRemote,
4+
BucketChecksum,
5+
Checkpoint,
6+
ColumnType,
7+
DBAdapter,
8+
isStreamingSyncCheckpoint,
9+
isStreamingSyncCheckpointDiff,
10+
isStreamingSyncData,
11+
PowerSyncControlCommand,
12+
SqliteBucketStorage,
13+
StreamingSyncLine,
14+
SyncDataBucket
15+
} from '@powersync/web';
16+
import { DynamicSchemaManager } from './DynamicSchemaManager';
17+
18+
export class RustClientInterceptor extends SqliteBucketStorage {
19+
private rdb: DBAdapter;
20+
private lastStartedCheckpoint: Checkpoint | null = null;
21+
22+
public tables: Record<string, Record<string, ColumnType>> = {};
23+
24+
constructor(
25+
db: DBAdapter,
26+
private remote: AbstractRemote,
27+
private schemaManager: DynamicSchemaManager
28+
) {
29+
super(db, (AbstractPowerSyncDatabase as any).transactionMutex);
30+
this.rdb = db;
31+
}
32+
33+
async control(op: PowerSyncControlCommand, payload: string | Uint8Array | ArrayBuffer | null): Promise<string> {
34+
const response = await super.control(op, payload);
35+
36+
if (op == PowerSyncControlCommand.PROCESS_TEXT_LINE) {
37+
await this.processTextLine(payload as string);
38+
} else if (op == PowerSyncControlCommand.PROCESS_BSON_LINE) {
39+
await this.processBinaryLine(payload as Uint8Array);
40+
}
41+
42+
return response;
43+
}
44+
45+
private processTextLine(line: string) {
46+
return this.processParsedLine(JSON.parse(line));
47+
}
48+
49+
private async processBinaryLine(line: Uint8Array) {
50+
const bson = await this.remote.getBSON();
51+
await this.processParsedLine(bson.deserialize(line) as StreamingSyncLine);
52+
}
53+
54+
private async processParsedLine(line: StreamingSyncLine) {
55+
if (isStreamingSyncCheckpoint(line)) {
56+
this.lastStartedCheckpoint = line.checkpoint;
57+
await this.trackCheckpoint(line.checkpoint);
58+
} else if (isStreamingSyncCheckpointDiff(line) && this.lastStartedCheckpoint) {
59+
const diff = line.checkpoint_diff;
60+
const newBuckets = new Map<string, BucketChecksum>();
61+
for (const checksum of this.lastStartedCheckpoint.buckets) {
62+
newBuckets.set(checksum.bucket, checksum);
63+
}
64+
for (const checksum of diff.updated_buckets) {
65+
newBuckets.set(checksum.bucket, checksum);
66+
}
67+
for (const bucket of diff.removed_buckets) {
68+
newBuckets.delete(bucket);
69+
}
70+
71+
const newCheckpoint: Checkpoint = {
72+
last_op_id: diff.last_op_id,
73+
buckets: [...newBuckets.values()],
74+
write_checkpoint: diff.write_checkpoint
75+
};
76+
this.lastStartedCheckpoint = newCheckpoint;
77+
await this.trackCheckpoint(newCheckpoint);
78+
} else if (isStreamingSyncData(line)) {
79+
const batch = { buckets: [SyncDataBucket.fromRow(line.data)] };
80+
81+
await this.rdb.writeTransaction(async (tx) => {
82+
for (const bucket of batch.buckets) {
83+
// Record metrics
84+
const size = JSON.stringify(bucket.data).length;
85+
await tx.execute(
86+
`UPDATE local_bucket_data SET
87+
download_size = IFNULL(download_size, 0) + ?,
88+
last_op = ?,
89+
downloading = ?,
90+
downloaded_operations = IFNULL(downloaded_operations, 0) + ?
91+
WHERE id = ?`,
92+
[size, bucket.next_after, bucket.has_more, bucket.data.length, bucket.bucket]
93+
);
94+
}
95+
});
96+
97+
await this.schemaManager.updateFromOperations(batch);
98+
}
99+
}
100+
101+
private async trackCheckpoint(checkpoint: Checkpoint) {
102+
await this.rdb.writeTransaction(async (tx) => {
103+
for (const bucket of checkpoint.buckets) {
104+
await tx.execute(
105+
`INSERT OR REPLACE INTO local_bucket_data(id, total_operations, last_op, download_size, downloading, downloaded_operations)
106+
VALUES (
107+
?,
108+
?,
109+
IFNULL((SELECT last_op FROM local_bucket_data WHERE id = ?), '0'),
110+
IFNULL((SELECT download_size FROM local_bucket_data WHERE id = ?), 0),
111+
IFNULL((SELECT downloading FROM local_bucket_data WHERE id = ?), TRUE),
112+
IFNULL((SELECT downloaded_operations FROM local_bucket_data WHERE id = ?), TRUE)
113+
)`,
114+
[bucket.bucket, bucket.count, bucket.bucket, bucket.bucket, bucket.bucket, bucket.bucket]
115+
);
116+
}
117+
});
118+
}
119+
}

tools/diagnostics-app/src/library/powersync/TokenConnector.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AbstractPowerSyncDatabase, PowerSyncBackendConnector } from '@powersync/web';
22
import { connect } from './ConnectionManager';
3+
import { LoginDetailsFormValues } from '@/components/widgets/LoginDetailsWidget';
34

45
export interface Credentials {
56
token: string;
@@ -21,11 +22,12 @@ export class TokenConnector implements PowerSyncBackendConnector {
2122
await tx?.complete();
2223
}
2324

24-
async signIn(credentials: Credentials) {
25+
async signIn(credentials: LoginDetailsFormValues) {
2526
validateSecureContext(credentials.endpoint);
2627
checkJWT(credentials.token);
2728
try {
2829
localStorage.setItem('powersync_credentials', JSON.stringify(credentials));
30+
localStorage.setItem('preferred_client_implementation', credentials.clientImplementation);
2931
await connect();
3032
} catch (e) {
3133
this.clearCredentials();
@@ -39,6 +41,7 @@ export class TokenConnector implements PowerSyncBackendConnector {
3941

4042
clearCredentials() {
4143
localStorage.removeItem('powersync_credentials');
44+
localStorage.removeItem('preferred_client_implementation');
4245
}
4346
}
4447

0 commit comments

Comments
 (0)