Skip to content

Commit 065aba6

Browse files
stevensJourneyChriztiaanmugikhan
authored
feat: Configurable Web VFS (added OPFS) (#418)
Co-authored-by: Christiaan Landman <[email protected]> Co-authored-by: Mughees Khan <[email protected]>
1 parent e79ed9b commit 065aba6

29 files changed

+3038
-1179
lines changed

.changeset/kind-suns-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/web': minor
3+
---
4+
5+
Added support for OPFS virtual filesystem.

demos/angular-supabase-todolist/src/app/powersync.service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
PowerSyncDatabase,
1010
Schema,
1111
Table,
12-
WASQLiteOpenFactory
12+
WASQLiteOpenFactory,
13+
WASQLiteVFS
1314
} from '@powersync/web';
1415

1516
export interface ListRecord {
@@ -66,14 +67,15 @@ export class PowerSyncService {
6667
constructor() {
6768
const factory = new WASQLiteOpenFactory({
6869
dbFilename: 'test.db',
69-
70+
vfs: WASQLiteVFS.OPFSCoopSyncVFS,
7071
// Specify the path to the worker script
7172
worker: 'assets/@powersync/worker/WASQLiteDB.umd.js'
7273
});
7374

7475
this.db = new PowerSyncDatabase({
7576
schema: AppSchema,
7677
database: factory,
78+
7779
sync: {
7880
// Specify the path to the worker script
7981
worker: 'assets/@powersync/worker/SharedSyncImplementation.umd.js'

demos/react-supabase-todolist/src/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<html lang="en">
22
<head>
33
<meta name="theme-color" content="#c44eff" />
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
45
<link rel="apple-touch-icon" href="/icons/icon.png" />
56
<link rel="stylesheet" href="./app/globals.css" />
67
<script type="module" src="./app/index.tsx"></script>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default defineConfig({
2525
// Don't optimize these packages as they contain web workers and WASM files.
2626
// https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673
2727
exclude: ['@journeyapps/wa-sqlite', '@powersync/web'],
28-
include: [],
28+
include: []
2929
// include: ['@powersync/web > js-logger'], // <-- Include `js-logger` when it isn't installed and imported.
3030
},
3131
plugins: [

packages/web/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,39 @@ Install it in your app with:
2828
npm install @journeyapps/wa-sqlite
2929
```
3030

31+
### Encryption with Multiple Ciphers
32+
33+
To enable encryption you need to specify an encryption key when instantiating the PowerSync database.
34+
35+
> The PowerSync Web SDK uses the ChaCha20 cipher algorithm by [default](https://utelle.github.io/SQLite3MultipleCiphers/docs/ciphers/cipher_chacha20/).
36+
37+
```typescript
38+
export const db = new PowerSyncDatabase({
39+
// The schema you defined
40+
schema: AppSchema,
41+
database: {
42+
// Filename for the SQLite database — it's important to only instantiate one instance per file.
43+
dbFilename: 'example.db'
44+
// Optional. Directory where the database file is located.'
45+
// dbLocation: 'path/to/directory'
46+
},
47+
// Encryption key for the database.
48+
encryptionKey: 'your-encryption-key'
49+
});
50+
51+
// If you are using a custom WASQLiteOpenFactory or WASQLiteDBAdapter, you need specify the encryption key inside the construtor
52+
export const db = new PowerSyncDatabase({
53+
schema: AppSchema,
54+
database: new WASQLiteOpenFactory({
55+
//new WASQLiteDBAdapter
56+
dbFilename: 'example.db',
57+
vfs: WASQLiteVFS.OPFSCoopSyncVFS,
58+
// Encryption key for the database.
59+
encryptionKey: 'your-encryption-key'
60+
})
61+
});
62+
```
63+
3164
## Webpack
3265

3366
See the [example Webpack config](https://github.com/powersync-ja/powersync-js/blob/main/demos/example-webpack/webpack.config.js) for details on polyfills and requirements.

packages/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"@powersync/common": "workspace:*",
6868
"async-mutex": "^0.4.0",
6969
"bson": "^6.6.0",
70-
"comlink": "^4.4.1",
70+
"comlink": "^4.4.2",
7171
"commander": "^12.1.0",
7272
"js-logger": "^1.6.1"
7373
},

packages/web/src/db/PowerSyncDatabase.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
AbstractPowerSyncDatabase,
77
DBAdapter,
88
DEFAULT_POWERSYNC_CLOSE_OPTIONS,
9+
isDBAdapter,
10+
isSQLOpenFactory,
911
PowerSyncDatabaseOptions,
1012
PowerSyncDatabaseOptionsWithDBAdapter,
1113
PowerSyncDatabaseOptionsWithOpenFactory,
@@ -14,21 +16,22 @@ import {
1416
StreamingSyncImplementation
1517
} from '@powersync/common';
1618
import { Mutex } from 'async-mutex';
19+
import { getNavigatorLocks } from '../shared/navigator';
1720
import { WASQLiteOpenFactory } from './adapters/wa-sqlite/WASQLiteOpenFactory';
1821
import {
1922
DEFAULT_WEB_SQL_FLAGS,
2023
ResolvedWebSQLOpenOptions,
2124
resolveWebSQLFlags,
2225
WebSQLFlags
2326
} from './adapters/web-sql-flags';
27+
import { WebDBAdapter } from './adapters/WebDBAdapter';
2428
import { SharedWebStreamingSyncImplementation } from './sync/SharedWebStreamingSyncImplementation';
2529
import { SSRStreamingSyncImplementation } from './sync/SSRWebStreamingSyncImplementation';
2630
import { WebRemote } from './sync/WebRemote';
2731
import {
2832
WebStreamingSyncImplementation,
2933
WebStreamingSyncImplementationOptions
3034
} from './sync/WebStreamingSyncImplementation';
31-
import { getNavigatorLocks } from '../shared/navigator';
3235

3336
export interface WebPowerSyncFlags extends WebSQLFlags {
3437
/**
@@ -55,14 +58,24 @@ type WithWebSyncOptions<Base> = Base & {
5558
sync?: WebSyncOptions;
5659
};
5760

61+
export interface WebEncryptionOptions {
62+
/**
63+
* Encryption key for the database.
64+
* If set, the database will be encrypted using Multiple Ciphers.
65+
*/
66+
encryptionKey?: string;
67+
}
68+
69+
type WithWebEncryptionOptions<Base> = Base & WebEncryptionOptions;
70+
5871
export type WebPowerSyncDatabaseOptionsWithAdapter = WithWebSyncOptions<
5972
WithWebFlags<PowerSyncDatabaseOptionsWithDBAdapter>
6073
>;
6174
export type WebPowerSyncDatabaseOptionsWithOpenFactory = WithWebSyncOptions<
6275
WithWebFlags<PowerSyncDatabaseOptionsWithOpenFactory>
6376
>;
6477
export type WebPowerSyncDatabaseOptionsWithSettings = WithWebSyncOptions<
65-
WithWebFlags<PowerSyncDatabaseOptionsWithSettings>
78+
WithWebFlags<WithWebEncryptionOptions<PowerSyncDatabaseOptionsWithSettings>>
6679
>;
6780

6881
export type WebPowerSyncDatabaseOptions = WithWebSyncOptions<WithWebFlags<PowerSyncDatabaseOptions>>;
@@ -72,14 +85,28 @@ export const DEFAULT_POWERSYNC_FLAGS: Required<WebPowerSyncFlags> = {
7285
externallyUnload: false
7386
};
7487

75-
export const resolveWebPowerSyncFlags = (flags?: WebPowerSyncFlags): WebPowerSyncFlags => {
88+
export const resolveWebPowerSyncFlags = (flags?: WebPowerSyncFlags): Required<WebPowerSyncFlags> => {
7689
return {
7790
...DEFAULT_POWERSYNC_FLAGS,
7891
...flags,
7992
...resolveWebSQLFlags(flags)
8093
};
8194
};
8295

96+
/**
97+
* Asserts that the database options are valid for custom database constructors.
98+
*/
99+
function assertValidDatabaseOptions(options: WebPowerSyncDatabaseOptions): void {
100+
if ('database' in options && 'encryptionKey' in options) {
101+
const { database } = options;
102+
if (isSQLOpenFactory(database) || isDBAdapter(database)) {
103+
throw new Error(
104+
`Invalid configuration: 'encryptionKey' should only be included inside the database object when using a custom ${isSQLOpenFactory(database) ? 'WASQLiteOpenFactory' : 'WASQLiteDBAdapter'} constructor.`
105+
);
106+
}
107+
}
108+
}
109+
83110
/**
84111
* A PowerSync database which provides SQLite functionality
85112
* which is automatically synced.
@@ -107,6 +134,8 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
107134
constructor(protected options: WebPowerSyncDatabaseOptions) {
108135
super(options);
109136

137+
assertValidDatabaseOptions(options);
138+
110139
this.resolvedFlags = resolveWebPowerSyncFlags(options.flags);
111140

112141
if (this.resolvedFlags.enableMultiTabs && !this.resolvedFlags.externallyUnload) {
@@ -120,7 +149,8 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
120149
protected openDBAdapter(options: WebPowerSyncDatabaseOptionsWithSettings): DBAdapter {
121150
const defaultFactory = new WASQLiteOpenFactory({
122151
...options.database,
123-
flags: resolveWebPowerSyncFlags(options.flags)
152+
flags: resolveWebPowerSyncFlags(options.flags),
153+
encryptionKey: options.encryptionKey
124154
});
125155
return defaultFactory.openDB();
126156
}
@@ -191,7 +221,10 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
191221
const logger = this.options.logger;
192222
logger ? logger.warn(warning) : console.warn(warning);
193223
}
194-
return new SharedWebStreamingSyncImplementation(syncOptions);
224+
return new SharedWebStreamingSyncImplementation({
225+
...syncOptions,
226+
db: this.database as WebDBAdapter // This should always be the case
227+
});
195228
default:
196229
return new WebStreamingSyncImplementation(syncOptions);
197230
}

packages/web/src/db/adapters/AbstractWebSQLOpenFactory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { DBAdapter, SQLOpenFactory } from '@powersync/common';
2+
import Logger, { ILogger } from 'js-logger';
23
import { SSRDBAdapter } from './SSRDBAdapter';
34
import { ResolvedWebSQLFlags, WebSQLOpenFactoryOptions, isServerSide, resolveWebSQLFlags } from './web-sql-flags';
45

56
export abstract class AbstractWebSQLOpenFactory implements SQLOpenFactory {
67
protected resolvedFlags: ResolvedWebSQLFlags;
8+
protected logger: ILogger;
79

810
constructor(protected options: WebSQLOpenFactoryOptions) {
911
this.resolvedFlags = resolveWebSQLFlags(options.flags);
12+
this.logger = options.logger ?? Logger.get(`AbstractWebSQLOpenFactory - ${this.options.dbFilename}`);
1013
}
1114

1215
/**
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { BatchedUpdateNotification, QueryResult } from '@powersync/common';
2+
import { ResolvedWebSQLOpenOptions } from './web-sql-flags';
3+
4+
/**
5+
* Proxied query result does not contain a function for accessing row values
6+
*/
7+
export type ProxiedQueryResult = Omit<QueryResult, 'rows'> & {
8+
rows: {
9+
_array: any[];
10+
length: number;
11+
};
12+
};
13+
export type OnTableChangeCallback = (event: BatchedUpdateNotification) => void;
14+
15+
/**
16+
* @internal
17+
* An async Database connection which provides basic async SQL methods.
18+
* This is usually a proxied through a web worker.
19+
*/
20+
export interface AsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> {
21+
init(): Promise<void>;
22+
close(): Promise<void>;
23+
execute(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
24+
executeBatch(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
25+
registerOnTableChange(callback: OnTableChangeCallback): Promise<() => void>;
26+
getConfig(): Promise<Config>;
27+
}
28+
29+
export type OpenAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> = (
30+
config: Config
31+
) => AsyncDatabaseConnection;

0 commit comments

Comments
 (0)