@powersync/web 2.4.2 → 2.6.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.
Files changed (98) hide show
  1. package/dist/index.react_native_web.js +142 -25
  2. package/dist/index.react_native_web.js.map +1 -1
  3. package/dist/worker/AccessHandlePoolVFS-Do8lc5Ck.js +2 -0
  4. package/dist/worker/AccessHandlePoolVFS-Do8lc5Ck.js.map +1 -0
  5. package/dist/worker/FacadeVFS-d1ZDvud7.js.map +1 -1
  6. package/dist/worker/IDBBatchAtomicVFS-CDb4d4wN.js +2 -0
  7. package/dist/worker/IDBBatchAtomicVFS-CDb4d4wN.js.map +1 -0
  8. package/dist/worker/MemoryVFS-DVJL5F8j.js.map +1 -1
  9. package/dist/worker/OPFSCoopSyncVFS-CDNBPKfy.js +2 -0
  10. package/dist/worker/OPFSCoopSyncVFS-CDNBPKfy.js.map +1 -0
  11. package/dist/worker/OPFSWriteAheadVFS-V9yBrLWl.js +2 -0
  12. package/dist/worker/OPFSWriteAheadVFS-V9yBrLWl.js.map +1 -0
  13. package/dist/worker/assets/mc-wa-sqlite-Cq3AePIW.wasm +0 -0
  14. package/dist/worker/assets/{mc-wa-sqlite-async-DdlQmAUV.wasm → mc-wa-sqlite-async-aRcIGeKj.wasm} +0 -0
  15. package/dist/worker/assets/wa-sqlite-CePoIZt_.wasm +0 -0
  16. package/dist/worker/assets/wa-sqlite-async-B3FiUnt1.wasm +0 -0
  17. package/dist/worker/{mc-wa-sqlite-PubB6u8c.js → mc-wa-sqlite-DRR2R_uj.js} +2 -2
  18. package/dist/worker/mc-wa-sqlite-DRR2R_uj.js.map +1 -0
  19. package/dist/worker/{mc-wa-sqlite-async-DJG_ANKO.js → mc-wa-sqlite-async-C40l6kIz.js} +2 -2
  20. package/dist/worker/mc-wa-sqlite-async-C40l6kIz.js.map +1 -0
  21. package/dist/worker/{wa-sqlite-D1_4mGs8.js → wa-sqlite-_a9CU4MM.js} +2 -2
  22. package/dist/worker/wa-sqlite-_a9CU4MM.js.map +1 -0
  23. package/dist/worker/wa-sqlite-async-Da0d0GJg.js +2 -0
  24. package/dist/worker/wa-sqlite-async-Da0d0GJg.js.map +1 -0
  25. package/dist/worker/{websockets-Q8W_lerF.js → websockets-Dx9ExO-W.js} +2 -2
  26. package/dist/worker/{websockets-Q8W_lerF.js.map → websockets-Dx9ExO-W.js.map} +1 -1
  27. package/dist/worker/worker.js +1 -1
  28. package/dist/worker/worker.js.map +1 -1
  29. package/lib/db/PowerSyncDatabase.d.ts +4 -3
  30. package/lib/db/PowerSyncDatabase.js +9 -2
  31. package/lib/db/PowerSyncDatabase.js.map +1 -1
  32. package/lib/db/adapters/options.d.ts +30 -0
  33. package/lib/db/adapters/options.js.map +1 -1
  34. package/lib/db/adapters/resolveAndValidateOptions.js +6 -1
  35. package/lib/db/adapters/resolveAndValidateOptions.js.map +1 -1
  36. package/lib/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +5 -0
  37. package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js +7 -2
  38. package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js.map +1 -1
  39. package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +4 -2
  40. package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js.map +1 -1
  41. package/lib/db/adapters/wa-sqlite/vfs.d.ts +24 -1
  42. package/lib/db/adapters/wa-sqlite/vfs.js +33 -4
  43. package/lib/db/adapters/wa-sqlite/vfs.js.map +1 -1
  44. package/lib/db/sync/SharedWebStreamingSyncImplementation.d.ts +2 -0
  45. package/lib/db/sync/SharedWebStreamingSyncImplementation.js +43 -12
  46. package/lib/db/sync/SharedWebStreamingSyncImplementation.js.map +1 -1
  47. package/lib/db/sync/WebRemote.d.ts +2 -3
  48. package/lib/db/sync/WebRemote.js +0 -2
  49. package/lib/db/sync/WebRemote.js.map +1 -1
  50. package/lib/db/sync/WebStreamingSyncImplementation.d.ts +8 -1
  51. package/lib/db/sync/WebStreamingSyncImplementation.js +14 -0
  52. package/lib/db/sync/WebStreamingSyncImplementation.js.map +1 -1
  53. package/lib/devtools/index.d.ts +7 -0
  54. package/lib/devtools/index.js +8 -0
  55. package/lib/devtools/index.js.map +1 -0
  56. package/lib/devtools/registry.d.ts +12 -0
  57. package/lib/devtools/registry.js +42 -0
  58. package/lib/devtools/registry.js.map +1 -0
  59. package/lib/worker/sync/BroadcastLogger.d.ts +1 -4
  60. package/lib/worker/sync/BroadcastLogger.js +14 -15
  61. package/lib/worker/sync/BroadcastLogger.js.map +1 -1
  62. package/lib/worker/sync/SharedSyncImplementation.d.ts +9 -2
  63. package/lib/worker/sync/SharedSyncImplementation.js +66 -37
  64. package/lib/worker/sync/SharedSyncImplementation.js.map +1 -1
  65. package/lib/worker/sync/WorkerClient.d.ts +3 -3
  66. package/lib/worker/sync/WorkerClient.js +3 -3
  67. package/lib/worker/sync/WorkerClient.js.map +1 -1
  68. package/package.json +12 -8
  69. package/src/db/PowerSyncDatabase.ts +12 -5
  70. package/src/db/adapters/options.ts +32 -0
  71. package/src/db/adapters/resolveAndValidateOptions.ts +9 -1
  72. package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +12 -2
  73. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +6 -2
  74. package/src/db/adapters/wa-sqlite/vfs.ts +51 -4
  75. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +54 -17
  76. package/src/db/sync/WebRemote.ts +2 -5
  77. package/src/db/sync/WebStreamingSyncImplementation.ts +18 -0
  78. package/src/devtools/index.ts +7 -0
  79. package/src/devtools/registry.ts +53 -0
  80. package/src/worker/sync/BroadcastLogger.ts +15 -15
  81. package/src/worker/sync/SharedSyncImplementation.ts +89 -48
  82. package/src/worker/sync/WorkerClient.ts +4 -3
  83. package/dist/worker/AccessHandlePoolVFS-BPUHfZME.js +0 -2
  84. package/dist/worker/AccessHandlePoolVFS-BPUHfZME.js.map +0 -1
  85. package/dist/worker/IDBBatchAtomicVFS-DbkDb777.js +0 -2
  86. package/dist/worker/IDBBatchAtomicVFS-DbkDb777.js.map +0 -1
  87. package/dist/worker/OPFSCoopSyncVFS-BgTiWPfa.js +0 -2
  88. package/dist/worker/OPFSCoopSyncVFS-BgTiWPfa.js.map +0 -1
  89. package/dist/worker/OPFSWriteAheadVFS-BW9pqSNn.js +0 -2
  90. package/dist/worker/OPFSWriteAheadVFS-BW9pqSNn.js.map +0 -1
  91. package/dist/worker/assets/mc-wa-sqlite-BHScYYZw.wasm +0 -0
  92. package/dist/worker/assets/wa-sqlite-CIxTmRHQ.wasm +0 -0
  93. package/dist/worker/assets/wa-sqlite-async-zc68fxQq.wasm +0 -0
  94. package/dist/worker/mc-wa-sqlite-PubB6u8c.js.map +0 -1
  95. package/dist/worker/mc-wa-sqlite-async-DJG_ANKO.js.map +0 -1
  96. package/dist/worker/wa-sqlite-D1_4mGs8.js.map +0 -1
  97. package/dist/worker/wa-sqlite-async-BxkwNY-2.js +0 -2
  98. package/dist/worker/wa-sqlite-async-BxkwNY-2.js.map +0 -1
@@ -64,6 +64,17 @@ export interface WebSpecificOpenOptions {
64
64
 
65
65
  vfs: WASQLiteVFS;
66
66
 
67
+ /**
68
+ * Names of additional no-argument WebAssembly exports to call after the PowerSync core initializer and before
69
+ * opening each SQLite connection, in array order. Each must register a statically linked extension, return
70
+ * SQLITE_OK (0) on success, and be safe to call more than once for a module.
71
+ *
72
+ * Requires a custom wa-sqlite build exporting these functions and a worker bundled against its matching JavaScript.
73
+ * Use exported names without Emscripten's leading underscore, e.g. `decimal_init_static`.
74
+ * Defaults to no additional initialization functions.
75
+ */
76
+ additionalInitializationFunctions?: readonly string[];
77
+
67
78
  /**
68
79
  * If the {@link vfs} supports it, an additional amount of read-only connections to open. Using additional read
69
80
  * connections can speed up queries by dispatching them to multiple workers running them concurrently.
@@ -96,6 +107,27 @@ export interface WebSpecificOpenOptions {
96
107
  * Defaults to 0 (disabling the cache).
97
108
  */
98
109
  preparedStatementsCache?: number;
110
+
111
+ /**
112
+ * The name of a [Storage Bucket](https://wicg.github.io/storage-buckets/) to keep the database files in,
113
+ * instead of the default bucket of the origin private file system.
114
+ *
115
+ * Only supported by the OPFS-based VFS implementations: {@link WASQLiteVFS.OPFSCoopSyncVFS},
116
+ * {@link WASQLiteVFS.OPFSWriteAheadVFS} and {@link WASQLiteVFS.AccessHandlePoolVFS}. Opening the database fails in
117
+ * a browser without the Storage Buckets API, so set this option only when `'storageBuckets' in navigator`.
118
+ *
119
+ * The SDK forwards the name to `navigator.storageBuckets.open()` unchanged, so the browser reports a name it does not
120
+ * accept. The bucket is opened by name, with the browser's default options. To configure options for the bucket,
121
+ * call and await `navigator.storageBuckets.open()` before opening a PowerSync database.
122
+ *
123
+ * A database in a bucket is a different database from one of the same name in the default bucket: setting this
124
+ * option for an existing database starts with an empty database, and the files in the default bucket are not
125
+ * moved. The SDK groups database resources by database name, without taking the storage bucket into account. To
126
+ * avoid lock contention and wrong update notifications, use unique database names even across buckets.
127
+ *
128
+ * Uses the default bucket when no option is given.
129
+ */
130
+ storageBucket?: string;
99
131
  }
100
132
 
101
133
  export interface ResolvedWebSQLOpenOptions extends SQLOpenOptions, WebSpecificOpenOptions {}
@@ -1,6 +1,6 @@
1
1
  import { LogLevels } from '@powersync/common';
2
2
  import { TemporaryStorageOption, WebSpecificOpenOptions } from './options.js';
3
- import { vfsRequiresDedicatedWorkers, WASQLiteVFS } from './wa-sqlite/vfs.js';
3
+ import { vfsRequiresDedicatedWorkers, vfsSupportsStorageBuckets, WASQLiteVFS } from './wa-sqlite/vfs.js';
4
4
 
5
5
  /**
6
6
  * The maximum length of a db filename we support.
@@ -45,5 +45,13 @@ export function resolveAndValidateOptions<And = {}>(
45
45
  );
46
46
  }
47
47
 
48
+ if (resolved.storageBucket != null) {
49
+ if (!vfsSupportsStorageBuckets(resolved.vfs)) {
50
+ throw new Error(
51
+ `Invalid configuration: The 'storageBucket' option needs an OPFS-based VFS, which ${resolved.vfs} is not.`
52
+ );
53
+ }
54
+ }
55
+
48
56
  return resolved;
49
57
  }
@@ -29,6 +29,11 @@ export interface RawWaSqliteDatabaseOptions {
29
29
  * The amount of prepared statements to cache, or 0 to disable caching.
30
30
  */
31
31
  preparedStatementsCache: number;
32
+ additionalInitializationFunctions?: readonly string[];
33
+ /**
34
+ * The name of a Storage Bucket to keep the database files in, for the OPFS-based VFS implementations.
35
+ */
36
+ storageBucket?: string;
32
37
  }
33
38
 
34
39
  /**
@@ -84,9 +89,14 @@ export class RawSqliteConnection {
84
89
  const sqlite3 = WaSqliteFactory(module);
85
90
  sqlite3.vfs_register(vfs, true);
86
91
  /**
87
- * Register the PowerSync core SQLite extension
92
+ * Register the PowerSync core and additional statically linked extensions
88
93
  */
89
- module.ccall('powersync_init_static', 'int', []);
94
+ for (const name of ['powersync_init_static', ...(this.options.additionalInitializationFunctions ?? [])]) {
95
+ const result = module.ccall(name, 'int', []);
96
+ if (result !== 0) {
97
+ throw new Error(`SQLite extension initializer ${name} failed with code ${result}`);
98
+ }
99
+ }
90
100
 
91
101
  /**
92
102
  * Create the multiple cipher vfs if an encryption key is provided
@@ -75,7 +75,9 @@ export class WASQLiteOpenFactory implements SQLOpenFactory {
75
75
  encryptionKey,
76
76
  temporaryStorage,
77
77
  cacheSizeKb,
78
- preparedStatementsCache
78
+ preparedStatementsCache,
79
+ additionalInitializationFunctions,
80
+ storageBucket
79
81
  } = this.options;
80
82
 
81
83
  if (!enableMultiTabs) {
@@ -95,7 +97,9 @@ export class WASQLiteOpenFactory implements SQLOpenFactory {
95
97
  temporaryStorage,
96
98
  cacheSizeKb,
97
99
  // TODO: Enable prepared statement cache by default?
98
- preparedStatementsCache: preparedStatementsCache ?? 0
100
+ preparedStatementsCache: preparedStatementsCache ?? 0,
101
+ additionalInitializationFunctions,
102
+ storageBucket
99
103
  };
100
104
  }
101
105
 
@@ -1,5 +1,6 @@
1
1
  import type * as SQLite from '@journeyapps/wa-sqlite';
2
2
  import { RawWaSqliteDatabaseOptions } from './RawSqliteConnection.js';
3
+ import type { WebSpecificOpenOptions } from '../options.js';
3
4
 
4
5
  /**
5
6
  * List of currently tested virtual filesystems
@@ -32,6 +33,51 @@ export function vfsRequiresDedicatedWorkers(vfs: WASQLiteVFS) {
32
33
  return vfs != WASQLiteVFS.IDBBatchAtomicVFS && vfs != WASQLiteVFS.InMemoryVfs;
33
34
  }
34
35
 
36
+ /**
37
+ * Whether the VFS can keep its files in a Storage Bucket, see {@link WebSpecificOpenOptions.storageBucket}.
38
+ */
39
+ export function vfsSupportsStorageBuckets(vfs: WASQLiteVFS) {
40
+ return (
41
+ vfs == WASQLiteVFS.OPFSCoopSyncVFS || vfs == WASQLiteVFS.OPFSWriteAheadVFS || vfs == WASQLiteVFS.AccessHandlePoolVFS
42
+ );
43
+ }
44
+
45
+ /**
46
+ * The part of the Storage Buckets API this module uses. The API is not in the TypeScript DOM library yet.
47
+ */
48
+ export interface StorageBucketManagerLike {
49
+ open(name: string): Promise<{ getDirectory(): Promise<FileSystemDirectoryHandle> }>;
50
+ }
51
+
52
+ /**
53
+ * Options for an OPFS-based VFS that keep its files in the named Storage Bucket, see
54
+ * {@link WebSpecificOpenOptions.storageBucket}. Without a bucket name, no options: the VFS uses the root of the
55
+ * origin private file system.
56
+ *
57
+ * @internal
58
+ */
59
+ export function vfsOptionsForStorageBucket(
60
+ storageBucket: string | undefined,
61
+ storageBuckets: StorageBucketManagerLike | null | undefined = (navigator as any).storageBuckets
62
+ ): { getRoot?: () => Promise<FileSystemDirectoryHandle>; prefix?: string } {
63
+ if (storageBucket == null) {
64
+ return {};
65
+ }
66
+ if (storageBuckets == null) {
67
+ throw new Error(
68
+ `The 'storageBucket' option needs the Storage Buckets API (navigator.storageBuckets), which this browser does not have. Check for it before setting the option.`
69
+ );
70
+ }
71
+ return {
72
+ // Called by the VFS each time it needs its root directory. Opening the bucket again creates it when the
73
+ // browser deleted it with the rest of the site's data.
74
+ getRoot: () => storageBuckets.open(storageBucket).then((bucket) => bucket.getDirectory()),
75
+ // Keeps the locks and channels of a file apart from those of a file of the same name in another bucket.
76
+ // Bucket names cannot contain ':', so the names of two buckets never overlap.
77
+ prefix: `${storageBucket}:`
78
+ };
79
+ }
80
+
35
81
  /**
36
82
  * @internal
37
83
  */
@@ -68,7 +114,8 @@ async function syncModuleFactory(encryptionKey: string | undefined): Promise<SQL
68
114
  export async function loadModuleAndVfs({
69
115
  vfs,
70
116
  filename,
71
- encryptionKey
117
+ encryptionKey,
118
+ storageBucket
72
119
  }: RawWaSqliteDatabaseOptions): Promise<{ module: SQLiteModule; vfs: SQLiteVFS }> {
73
120
  let moduleFactory = syncModuleFactory;
74
121
  let resolveVfs: (module: any) => Promise<SQLiteVFS>;
@@ -86,19 +133,19 @@ export async function loadModuleAndVfs({
86
133
  case WASQLiteVFS.AccessHandlePoolVFS: {
87
134
  // @ts-expect-error The types for this import are missing upstream
88
135
  const { AccessHandlePoolVFS } = await import('@journeyapps/wa-sqlite/src/examples/AccessHandlePoolVFS.js');
89
- resolveVfs = (module) => AccessHandlePoolVFS.create(filename, module);
136
+ resolveVfs = (module) => AccessHandlePoolVFS.create(filename, module, vfsOptionsForStorageBucket(storageBucket));
90
137
  break;
91
138
  }
92
139
  case WASQLiteVFS.OPFSCoopSyncVFS: {
93
140
  // @ts-expect-error The types for this import are missing upstream
94
141
  const { OPFSCoopSyncVFS } = await import('@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js');
95
- resolveVfs = (module) => OPFSCoopSyncVFS.create(filename, module);
142
+ resolveVfs = (module) => OPFSCoopSyncVFS.create(filename, module, vfsOptionsForStorageBucket(storageBucket));
96
143
  break;
97
144
  }
98
145
  case WASQLiteVFS.OPFSWriteAheadVFS: {
99
146
  // @ts-expect-error The types for this import are missing upstream
100
147
  const { OPFSWriteAheadVFS } = await import('@journeyapps/wa-sqlite/src/examples/OPFSWriteAheadVFS.js');
101
- resolveVfs = (module) => OPFSWriteAheadVFS.create(filename, module, {});
148
+ resolveVfs = (module) => OPFSWriteAheadVFS.create(filename, module, vfsOptionsForStorageBucket(storageBucket));
102
149
  break;
103
150
  }
104
151
  case WASQLiteVFS.InMemoryVfs: {
@@ -9,7 +9,12 @@ import {
9
9
  WebStreamingSyncImplementationOptions
10
10
  } from './WebStreamingSyncImplementation.js';
11
11
  import { generateTabCloseSignal } from '../../shared/tab_close_signal.js';
12
- import { SubscribedStream, SyncStatusJson, ResolvedSyncOptions } from '@powersync/shared-internals';
12
+ import {
13
+ SubscribedStream,
14
+ SyncStatusJson,
15
+ ResolvedSyncOptions,
16
+ serializeErrorForRelay
17
+ } from '@powersync/shared-internals';
13
18
  import { connectToExistingWorker, connectToWorker, WorkerConnection } from '../../worker/client.js';
14
19
 
15
20
  /**
@@ -30,12 +35,25 @@ class SharedSyncClientProvider extends AbstractSharedSyncClientProvider {
30
35
  return Comlink.transfer(port, [port]);
31
36
  }
32
37
 
38
+ async #relay<T>(action: () => Promise<T>): Promise<T> {
39
+ try {
40
+ return await action();
41
+ } catch (ex) {
42
+ /**
43
+ * Connector callbacks run in this tab and their rejection crosses the
44
+ * MessagePort to the sync worker, so serialize it here, where the error
45
+ * originates, before Comlink's error transfer can drop custom fields.
46
+ */
47
+ throw serializeErrorForRelay(ex);
48
+ }
49
+ }
50
+
33
51
  invalidateCredentials() {
34
52
  this.options.remote.invalidateCredentials();
35
53
  }
36
54
 
37
55
  async fetchCredentials(): Promise<PowerSyncCredentials | null> {
38
- const credentials = await this.options.remote.getCredentials();
56
+ const credentials = await this.#relay(() => this.options.remote.getCredentials());
39
57
  if (credentials == null) {
40
58
  return null;
41
59
  }
@@ -56,11 +74,13 @@ class SharedSyncClientProvider extends AbstractSharedSyncClientProvider {
56
74
  * Don't return anything here, just incase something which is not
57
75
  * serializable is returned from the `uploadCrud` function.
58
76
  */
59
- await this.options.uploadCrud();
77
+ await this.#relay(() => this.options.connector.uploadCrud!());
60
78
  }
61
79
 
62
80
  override async postCheckpointRequest(clientId: string, requestId: string): Promise<string | null> {
63
- return await this.options.postCheckpointRequest(clientId, requestId);
81
+ return await this.#relay(
82
+ async () => (await this.options.connector.postCheckpointRequest!(clientId, requestId)) ?? null
83
+ );
64
84
  }
65
85
 
66
86
  get logger() {
@@ -159,10 +179,14 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
159
179
  * - The client sends the params to the shared worker after locks have been registered.
160
180
  */
161
181
  const closeSignal = await generateTabCloseSignal(this.abortOnClose.signal);
162
- // Awaiting here ensures the worker is waiting for the lock
163
- await this.syncManager.addLockBasedCloseSignal(closeSignal);
182
+ const { identifier, connector } = this.options;
164
183
 
165
- const { identifier } = this.options;
184
+ // Awaiting here ensures the worker is waiting for the lock
185
+ await this.syncManager.initialize(closeSignal, {
186
+ hasFetchCredentials: !!connector.fetchCredentials,
187
+ hasPostCheckpointRequest: !!connector.postCheckpointRequest,
188
+ hasUploadCrud: !!connector.uploadCrud
189
+ });
166
190
 
167
191
  await this.syncManager.setParams(
168
192
  {
@@ -192,9 +216,30 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
192
216
  }
193
217
 
194
218
  async dispose(): Promise<void> {
195
- await this.waitForReady();
219
+ let registeredWithWorker = true;
220
+ try {
221
+ await this.waitForReady();
222
+ } catch {
223
+ // The worker rejected this client (e.g. because it connects in a mode incompatible with other tabs), so it won't
224
+ // acknowledge a close request either.
225
+ registeredWithWorker = false;
226
+ }
227
+
228
+ if (registeredWithWorker) {
229
+ await this.#closeWorkerClient();
230
+ }
196
231
 
197
- await new Promise<void>((resolve) => {
232
+ await super.dispose();
233
+
234
+ this.abortOnClose.abort();
235
+
236
+ // Release the proxy
237
+ this.syncManager[Comlink.releaseProxy]();
238
+ this.worker.close();
239
+ }
240
+
241
+ #closeWorkerClient() {
242
+ return new Promise<void>((resolve) => {
198
243
  // This will always be a message port since we use shared workers.
199
244
  const messagePort = this.worker.endpoint as MessagePort;
200
245
 
@@ -213,14 +258,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
213
258
  };
214
259
  messagePort.postMessage(closeMessagePayload);
215
260
  });
216
-
217
- await super.dispose();
218
-
219
- this.abortOnClose.abort();
220
-
221
- // Release the proxy
222
- this.syncManager[Comlink.releaseProxy]();
223
- this.worker.close();
224
261
  }
225
262
 
226
263
  async waitForReady() {
@@ -1,14 +1,11 @@
1
1
  import { LogLevels, PowerSyncLogger } from '@powersync/common';
2
- import { AbstractRemote, FetchOptions, RemoteConnector } from '@powersync/shared-internals';
2
+ import { AbstractRemote, FetchOptions, InternalConnector } from '@powersync/shared-internals';
3
3
 
4
4
  import { getUserAgentInfo } from './userAgent.js';
5
5
  import type { WebSocketSyncStreamPlatform, WebSocketSupport } from '@powersync/shared-internals/websockets';
6
6
 
7
7
  export class WebRemote extends AbstractRemote {
8
- constructor(
9
- protected connector: RemoteConnector,
10
- logger: PowerSyncLogger
11
- ) {
8
+ constructor(connector: InternalConnector, logger: PowerSyncLogger) {
12
9
  super(connector, logger);
13
10
  }
14
11
 
@@ -2,6 +2,7 @@ import { LogLevels } from '@powersync/common';
2
2
  import {
3
3
  AbstractStreamingSyncImplementation,
4
4
  AbstractStreamingSyncImplementationOptions,
5
+ DiagnosticsEvent,
5
6
  LockOptions,
6
7
  LockType
7
8
  } from '@powersync/shared-internals';
@@ -14,11 +15,28 @@ export interface WebStreamingSyncImplementationOptions extends AbstractStreaming
14
15
  }
15
16
 
16
17
  export class WebStreamingSyncImplementation extends AbstractStreamingSyncImplementation {
18
+ private diagnosticsChannel?: BroadcastChannel;
19
+
17
20
  constructor(options: WebStreamingSyncImplementationOptions) {
18
21
  // Super will store and provide default values for options
19
22
  super(options);
20
23
  }
21
24
 
25
+ /**
26
+ * Broadcasts a core diagnostics event on a same-origin channel for diagnostics tooling to consume.
27
+ * The channel reaches the page even when sync runs in a shared worker.
28
+ */
29
+ protected override emitDiagnostics(event: DiagnosticsEvent): void {
30
+ this.diagnosticsChannel ??= new BroadcastChannel('powersync-diagnostics-events');
31
+ this.diagnosticsChannel.postMessage(event);
32
+ }
33
+
34
+ override async dispose(): Promise<void> {
35
+ await super.dispose();
36
+ this.diagnosticsChannel?.close();
37
+ this.diagnosticsChannel = undefined;
38
+ }
39
+
22
40
  get webOptions(): WebStreamingSyncImplementationOptions {
23
41
  return this.options as WebStreamingSyncImplementationOptions;
24
42
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Development-tooling entry point (`@powersync/web/devtools`).
3
+ *
4
+ * Kept off the main index so the public API of `@powersync/web` is unchanged; tooling imports this
5
+ * subpath explicitly.
6
+ */
7
+ export { getRegisteredDatabases, observeRegisteredDatabases } from './registry.js';
@@ -0,0 +1,53 @@
1
+ import { BaseObserver, type BaseListener } from '@powersync/common';
2
+ import type { WebPowerSyncDatabase } from '../db/PowerSyncDatabase.js';
3
+
4
+ interface RegistryListener extends BaseListener {
5
+ databasesChanged: (databases: readonly WebPowerSyncDatabase[]) => void;
6
+ }
7
+
8
+ /**
9
+ * Live database instances in this page, for development tooling.
10
+ *
11
+ * Every {@link WebPowerSyncDatabase} registers itself when constructed and leaves when closed, so a
12
+ * tool that runs in the same page (for example the diagnostics Vite plugin's injected agent) can find
13
+ * the app's databases without the app wiring anything up. The set holds only instances that already
14
+ * exist, so it adds nothing to a production bundle beyond the references themselves.
15
+ */
16
+ class DatabaseRegistry extends BaseObserver<RegistryListener> {
17
+ readonly databases = new Set<WebPowerSyncDatabase>();
18
+
19
+ notify(): void {
20
+ const snapshot = [...this.databases];
21
+ this.iterateListeners((listener) => listener.databasesChanged?.(snapshot));
22
+ }
23
+ }
24
+
25
+ const registry = new DatabaseRegistry();
26
+
27
+ /** @internal Called by the database constructor. */
28
+ export function registerDatabase(db: WebPowerSyncDatabase): void {
29
+ registry.databases.add(db);
30
+ registry.notify();
31
+ }
32
+
33
+ /** @internal Called when the database closes. */
34
+ export function unregisterDatabase(db: WebPowerSyncDatabase): void {
35
+ if (registry.databases.delete(db)) {
36
+ registry.notify();
37
+ }
38
+ }
39
+
40
+ /** The databases currently open in this page. */
41
+ export function getRegisteredDatabases(): readonly WebPowerSyncDatabase[] {
42
+ return [...registry.databases];
43
+ }
44
+
45
+ /**
46
+ * Calls `listener` with the current databases now, and again whenever one is opened or closed.
47
+ * Returns a function that stops listening.
48
+ */
49
+ export function observeRegisteredDatabases(listener: (databases: readonly WebPowerSyncDatabase[]) => void): () => void {
50
+ const stop = registry.registerListener({ databasesChanged: listener });
51
+ listener([...registry.databases]);
52
+ return stop;
53
+ }
@@ -1,4 +1,5 @@
1
1
  import { PowerSyncLogger, LogLevels, CreateLoggerOptions, createConsoleLogger, LogRecord } from '@powersync/common';
2
+ import { serializeErrorForRelay } from '@powersync/shared-internals';
2
3
  import { type WrappedSyncPort } from './SharedSyncImplementation.js';
3
4
 
4
5
  /**
@@ -8,6 +9,8 @@ export class BroadcastLogger implements PowerSyncLogger {
8
9
  private readonly inner: PowerSyncLogger & CreateLoggerOptions;
9
10
  private currentLevel: number = LogLevels.info;
10
11
 
12
+ private sequence = 0;
13
+
11
14
  sendBroadcasts = true;
12
15
 
13
16
  constructor(
@@ -48,27 +51,24 @@ export class BroadcastLogger implements PowerSyncLogger {
48
51
  }
49
52
  }
50
53
 
51
- /**
52
- * Guards against any logging errors.
53
- * We don't want a logging exception to cause further issues upstream
54
- */
55
54
  protected sanitizeRecord(record: LogRecord): LogRecord {
56
- if (!record.error) {
55
+ // Preserve explicit undefined rejection reasons.
56
+ if (!('error' in record)) {
57
57
  return record;
58
58
  }
59
59
 
60
- let error;
61
- try {
62
- // Try and clone here first. If it fails it won't be passable over a MessagePort
63
- error = structuredClone(record.error);
64
- } catch (ex) {
65
- console.error(ex);
66
- error = 'Could not serialize log params. Check shared worker logs for more details.';
67
- }
68
-
60
+ const serialized = serializeErrorForRelay(record.error);
69
61
  return {
70
62
  ...record,
71
- error
63
+ error: {
64
+ ...serialized,
65
+ relay: {
66
+ origin: 'shared-sync-worker',
67
+ sequence: this.sequence++,
68
+ errorState: serialized.state,
69
+ relayedAt: new Date().toISOString()
70
+ }
71
+ }
72
72
  };
73
73
  }
74
74
  }