@powersync/web 1.32.0 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/web",
3
- "version": "1.32.0",
3
+ "version": "1.33.0",
4
4
  "description": "PowerSync Web SDK",
5
5
  "main": "lib/src/index.js",
6
6
  "module": "lib/src/index.js",
@@ -56,14 +56,14 @@
56
56
  "license": "Apache-2.0",
57
57
  "peerDependencies": {
58
58
  "@journeyapps/wa-sqlite": "^1.4.1",
59
- "@powersync/common": "^1.46.0"
59
+ "@powersync/common": "^1.47.0"
60
60
  },
61
61
  "dependencies": {
62
62
  "async-mutex": "^0.5.0",
63
63
  "bson": "^6.10.4",
64
64
  "comlink": "^4.4.2",
65
65
  "commander": "^12.1.0",
66
- "@powersync/common": "1.46.0"
66
+ "@powersync/common": "1.47.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@journeyapps/wa-sqlite": "^1.4.1",
@@ -0,0 +1,117 @@
1
+ import { AttachmentData, EncodingType, LocalStorageAdapter } from '@powersync/common';
2
+
3
+ /**
4
+ * IndexDBFileSystemStorageAdapter implements LocalStorageAdapter using IndexedDB.
5
+ * Suitable for web browsers and web-based environments.
6
+ */
7
+ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter {
8
+ private dbPromise!: Promise<IDBDatabase>;
9
+
10
+ constructor(private databaseName: string = 'PowerSyncFiles') {}
11
+
12
+ async initialize(): Promise<void> {
13
+ this.dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
14
+ const request = indexedDB.open(this.databaseName, 1);
15
+ request.onupgradeneeded = () => {
16
+ request.result.createObjectStore('files');
17
+ };
18
+ request.onsuccess = () => resolve(request.result);
19
+ request.onerror = () => reject(request.error);
20
+ });
21
+ }
22
+
23
+ async clear(): Promise<void> {
24
+ const db = await this.dbPromise;
25
+ return new Promise((resolve, reject) => {
26
+ const tx = db.transaction('files', 'readwrite');
27
+ const store = tx.objectStore('files');
28
+ const req = store.clear();
29
+ req.onsuccess = () => resolve();
30
+ req.onerror = () => reject(req.error);
31
+ });
32
+ }
33
+
34
+ getLocalUri(filename: string): string {
35
+ return `indexeddb://${this.databaseName}/files/${filename}`;
36
+ }
37
+
38
+ private async getStore(mode: IDBTransactionMode = 'readonly'): Promise<IDBObjectStore> {
39
+ const db = await this.dbPromise;
40
+ const tx = db.transaction('files', mode);
41
+ return tx.objectStore('files');
42
+ }
43
+
44
+ async saveFile(filePath: string, data: AttachmentData): Promise<number> {
45
+ const store = await this.getStore('readwrite');
46
+
47
+ let dataToStore: ArrayBuffer;
48
+ let size: number;
49
+
50
+ if (typeof data === 'string') {
51
+ const binaryString = atob(data);
52
+ const bytes = new Uint8Array(binaryString.length);
53
+ for (let i = 0; i < binaryString.length; i++) {
54
+ bytes[i] = binaryString.charCodeAt(i);
55
+ }
56
+ dataToStore = bytes.buffer;
57
+ size = bytes.byteLength;
58
+ } else {
59
+ dataToStore = data;
60
+ size = dataToStore.byteLength;
61
+ }
62
+
63
+ return await new Promise<number>((resolve, reject) => {
64
+ const req = store.put(dataToStore, filePath);
65
+ req.onsuccess = () => resolve(size);
66
+ req.onerror = () => reject(req.error);
67
+ });
68
+ }
69
+
70
+ async readFile(fileUri: string, options?: { encoding?: EncodingType; mediaType?: string }): Promise<ArrayBuffer> {
71
+ const store = await this.getStore();
72
+ return new Promise<ArrayBuffer>((resolve, reject) => {
73
+ const req = store.get(fileUri);
74
+ req.onsuccess = async () => {
75
+ if (!req.result) {
76
+ reject(new Error('File not found'));
77
+ return;
78
+ }
79
+
80
+ resolve(req.result as ArrayBuffer);
81
+ };
82
+ req.onerror = () => reject(req.error);
83
+ });
84
+ }
85
+
86
+ async deleteFile(uri: string, options?: { filename?: string }): Promise<void> {
87
+ const store = await this.getStore('readwrite');
88
+ await new Promise<void>((resolve, reject) => {
89
+ const req = store.delete(uri);
90
+ req.onsuccess = () => resolve();
91
+ req.onerror = () => reject(req.error);
92
+ });
93
+ }
94
+
95
+ async fileExists(fileUri: string): Promise<boolean> {
96
+ const store = await this.getStore();
97
+ return new Promise<boolean>((resolve, reject) => {
98
+ const req = store.get(fileUri);
99
+ req.onsuccess = () => resolve(!!req.result);
100
+ req.onerror = () => reject(req.error);
101
+ });
102
+ }
103
+
104
+ async makeDir(path: string): Promise<void> {
105
+ // No-op for IndexedDB as it does not have a directory structure
106
+ }
107
+
108
+ async rmDir(path: string): Promise<void> {
109
+ const store = await this.getStore('readwrite');
110
+ const range = IDBKeyRange.bound(path + '/', path + '/\uffff', false, false);
111
+ await new Promise<void>((resolve, reject) => {
112
+ const req = store.delete(range);
113
+ req.onsuccess = () => resolve();
114
+ req.onerror = () => reject(req.error);
115
+ });
116
+ }
117
+ }
@@ -151,13 +151,15 @@ export class LockedAsyncDatabaseAdapter
151
151
  * Returns a pending operation if one is already in progress.
152
152
  */
153
153
  async reOpenInternalDB(): Promise<void> {
154
- if (!this.options.reOpenOnConnectionClosed) {
155
- throw new Error(`Cannot re-open underlying database, reOpenOnConnectionClosed is not enabled`);
156
- }
157
- if (this.databaseOpenPromise) {
154
+ if (this.closing || !this.options.reOpenOnConnectionClosed) {
155
+ // No-op
156
+ return;
157
+ } else if (this.databaseOpenPromise) {
158
+ // Already busy opening
158
159
  return this.databaseOpenPromise;
160
+ } else {
161
+ return this._reOpen();
159
162
  }
160
- return this._reOpen();
161
163
  }
162
164
 
163
165
  protected async _init() {
@@ -317,9 +319,17 @@ export class LockedAsyncDatabaseAdapter
317
319
  protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
318
320
  await this.waitForInitialized();
319
321
 
320
- // The database is being opened in the background. Wait for it here.
322
+ // The database is being (re)opened in the background. Wait for it here.
321
323
  if (this.databaseOpenPromise) {
322
324
  await this.databaseOpenPromise;
325
+ } else if (!this._db) {
326
+ /**
327
+ * The database is not open anymore, we might need to re-open it.
328
+ * Typically, _db, can be `null` if we tried to reOpen the database, but failed to succeed in re-opening.
329
+ * This can happen when disconnecting the client.
330
+ * Note: It is safe to re-enter this method multiple times.
331
+ */
332
+ await this.reOpenInternalDB();
323
333
  }
324
334
 
325
335
  return this._acquireLock(async () => {
@@ -339,11 +349,9 @@ export class LockedAsyncDatabaseAdapter
339
349
  return await callback();
340
350
  } catch (ex) {
341
351
  if (ConnectionClosedError.MATCHES(ex)) {
342
- if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
343
- // Immediately re-open the database. We need to miss as little table updates as possible.
344
- // Note, don't await this since it uses the same lock as we're in now.
345
- this.reOpenInternalDB();
346
- }
352
+ // Immediately re-open the database. We need to miss as little table updates as possible.
353
+ // Note, don't await this since it uses the same lock as we're in now.
354
+ this.reOpenInternalDB();
347
355
  }
348
356
  throw ex;
349
357
  } finally {
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from '@powersync/common';
2
+ export * from './attachments/IndexDBFileSystemAdapter.js';
2
3
  export * from './db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.js';
3
4
  export * from './db/adapters/AbstractWebSQLOpenFactory.js';
4
5
  export * from './db/adapters/AsyncDatabaseConnection.js';