@powersync/web 0.0.0-dev-20260202162549 → 0.0.0-dev-20260216124709

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 (100) hide show
  1. package/dist/0b19af1befc07ce338dd.wasm +0 -0
  2. package/dist/2632c3bda9473da74fd5.wasm +0 -0
  3. package/dist/64f5351ba3784bfe2f3e.wasm +0 -0
  4. package/dist/9318ca94aac4d0fe0135.wasm +0 -0
  5. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js +1878 -0
  6. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js.map +1 -0
  7. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530150.index.umd.js +555 -0
  8. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530150.index.umd.js.map +1 -0
  9. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530151.index.umd.js +555 -0
  10. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530151.index.umd.js.map +1 -0
  11. package/dist/index.umd.js +8267 -0
  12. package/dist/index.umd.js.map +1 -0
  13. package/dist/worker/SharedSyncImplementation.umd.js +19059 -0
  14. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -0
  15. package/dist/worker/WASQLiteDB.umd.js +17736 -0
  16. package/dist/worker/WASQLiteDB.umd.js.map +1 -0
  17. package/dist/worker/node_modules_pnpm_bson_6_10_4_node_modules_bson_lib_bson_mjs.umd.js +4646 -0
  18. package/dist/worker/node_modules_pnpm_bson_6_10_4_node_modules_bson_lib_bson_mjs.umd.js.map +1 -0
  19. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-3a94cf.umd.js +44 -0
  20. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-3a94cf.umd.js.map +1 -0
  21. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-868779.umd.js +44 -0
  22. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-868779.umd.js.map +1 -0
  23. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-f60d0d.umd.js +44 -0
  24. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-f60d0d.umd.js.map +1 -0
  25. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +44 -0
  26. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -0
  27. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-0d2437.umd.js +2478 -0
  28. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-0d2437.umd.js.map +1 -0
  29. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-1d4e74.umd.js +1820 -0
  30. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-1d4e74.umd.js.map +1 -0
  31. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-3622cf.umd.js +1681 -0
  32. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-3622cf.umd.js.map +1 -0
  33. package/lib/package.json +95 -0
  34. package/lib/src/attachments/IndexDBFileSystemAdapter.d.ts +25 -0
  35. package/lib/src/attachments/IndexDBFileSystemAdapter.js +104 -0
  36. package/lib/src/db/NavigatorTriggerClaimManager.d.ts +6 -0
  37. package/lib/src/db/NavigatorTriggerClaimManager.js +20 -0
  38. package/lib/src/db/PowerSyncDatabase.d.ts +79 -0
  39. package/lib/src/db/PowerSyncDatabase.js +166 -0
  40. package/lib/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.d.ts +23 -0
  41. package/lib/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.js +26 -0
  42. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.d.ts +17 -0
  43. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.js +33 -0
  44. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +49 -0
  45. package/lib/src/db/adapters/AsyncDatabaseConnection.js +1 -0
  46. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +109 -0
  47. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +401 -0
  48. package/lib/src/db/adapters/SSRDBAdapter.d.ts +31 -0
  49. package/lib/src/db/adapters/SSRDBAdapter.js +69 -0
  50. package/lib/src/db/adapters/WebDBAdapter.d.ts +20 -0
  51. package/lib/src/db/adapters/WebDBAdapter.js +1 -0
  52. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +59 -0
  53. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +147 -0
  54. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.d.ts +12 -0
  55. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.js +19 -0
  56. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +155 -0
  57. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +401 -0
  58. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.d.ts +32 -0
  59. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +49 -0
  60. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +23 -0
  61. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +96 -0
  62. package/lib/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.d.ts +15 -0
  63. package/lib/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.js +21 -0
  64. package/lib/src/db/adapters/web-sql-flags.d.ts +87 -0
  65. package/lib/src/db/adapters/web-sql-flags.js +36 -0
  66. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +48 -0
  67. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +65 -0
  68. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +57 -0
  69. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +237 -0
  70. package/lib/src/db/sync/WebRemote.d.ts +9 -0
  71. package/lib/src/db/sync/WebRemote.js +44 -0
  72. package/lib/src/db/sync/WebStreamingSyncImplementation.d.ts +13 -0
  73. package/lib/src/db/sync/WebStreamingSyncImplementation.js +18 -0
  74. package/lib/src/db/sync/userAgent.d.ts +17 -0
  75. package/lib/src/db/sync/userAgent.js +64 -0
  76. package/lib/src/index.d.ts +15 -0
  77. package/lib/src/index.js +15 -0
  78. package/lib/src/shared/navigator.d.ts +1 -0
  79. package/lib/src/shared/navigator.js +6 -0
  80. package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +42 -0
  81. package/lib/src/worker/db/SharedWASQLiteConnection.js +90 -0
  82. package/lib/src/worker/db/WASQLiteDB.worker.d.ts +4 -0
  83. package/lib/src/worker/db/WASQLiteDB.worker.js +69 -0
  84. package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +9 -0
  85. package/lib/src/worker/db/WorkerWASQLiteConnection.js +12 -0
  86. package/lib/src/worker/db/open-worker-database.d.ts +14 -0
  87. package/lib/src/worker/db/open-worker-database.js +52 -0
  88. package/lib/src/worker/sync/AbstractSharedSyncClientProvider.d.ts +19 -0
  89. package/lib/src/worker/sync/AbstractSharedSyncClientProvider.js +5 -0
  90. package/lib/src/worker/sync/BroadcastLogger.d.ts +47 -0
  91. package/lib/src/worker/sync/BroadcastLogger.js +128 -0
  92. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +137 -0
  93. package/lib/src/worker/sync/SharedSyncImplementation.js +495 -0
  94. package/lib/src/worker/sync/SharedSyncImplementation.worker.d.ts +1 -0
  95. package/lib/src/worker/sync/SharedSyncImplementation.worker.js +11 -0
  96. package/lib/src/worker/sync/WorkerClient.d.ts +31 -0
  97. package/lib/src/worker/sync/WorkerClient.js +84 -0
  98. package/lib/tsconfig.tsbuildinfo +1 -0
  99. package/package.json +3 -3
  100. package/src/index.ts +1 -0
@@ -0,0 +1,137 @@
1
+ import { BaseObserver, ConnectionManager, DBAdapter, SubscribedStream, SyncStatus, type ILogLevel, type ILogger, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions } from '@powersync/common';
2
+ import { Mutex } from 'async-mutex';
3
+ import * as Comlink from 'comlink';
4
+ import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from '../../db/sync/WebStreamingSyncImplementation.js';
5
+ import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection.js';
6
+ import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags.js';
7
+ import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider.js';
8
+ /**
9
+ * @internal
10
+ * Manual message events for shared sync clients
11
+ */
12
+ export declare enum SharedSyncClientEvent {
13
+ /**
14
+ * This client requests the shared sync manager should
15
+ * close it's connection to the client.
16
+ */
17
+ CLOSE_CLIENT = "close-client",
18
+ CLOSE_ACK = "close-ack"
19
+ }
20
+ /**
21
+ * @internal
22
+ */
23
+ export type ManualSharedSyncPayload = {
24
+ event: SharedSyncClientEvent;
25
+ data: any;
26
+ };
27
+ /**
28
+ * @internal
29
+ */
30
+ export type SharedSyncInitOptions = {
31
+ streamOptions: Omit<WebStreamingSyncImplementationOptions, 'adapter' | 'uploadCrud' | 'remote' | 'subscriptions'>;
32
+ dbParams: ResolvedWebSQLOpenOptions;
33
+ };
34
+ /**
35
+ * @internal
36
+ */
37
+ export interface SharedSyncImplementationListener extends StreamingSyncImplementationListener {
38
+ initialized: () => void;
39
+ }
40
+ /**
41
+ * @internal
42
+ */
43
+ export type WrappedSyncPort = {
44
+ port: MessagePort;
45
+ clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
46
+ db?: DBAdapter;
47
+ currentSubscriptions: SubscribedStream[];
48
+ closeListeners: (() => void | Promise<void>)[];
49
+ isClosing: boolean;
50
+ };
51
+ /**
52
+ * @internal
53
+ */
54
+ export type RemoteOperationAbortController = {
55
+ controller: AbortController;
56
+ activePort: WrappedSyncPort;
57
+ };
58
+ /**
59
+ * @internal
60
+ * Shared sync implementation which runs inside a shared webworker
61
+ */
62
+ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImplementationListener> {
63
+ protected ports: WrappedSyncPort[];
64
+ protected isInitialized: Promise<void>;
65
+ protected statusListener?: () => void;
66
+ protected fetchCredentialsController?: RemoteOperationAbortController;
67
+ protected uploadDataController?: RemoteOperationAbortController;
68
+ protected syncParams: SharedSyncInitOptions | null;
69
+ protected logger: ILogger;
70
+ protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
71
+ protected portMutex: Mutex;
72
+ private subscriptions;
73
+ protected connectionManager: ConnectionManager;
74
+ syncStatus: SyncStatus;
75
+ broadCastLogger: ILogger;
76
+ protected distributedDB: DBAdapter | null;
77
+ constructor();
78
+ get lastSyncedAt(): Date | undefined;
79
+ get isConnected(): boolean;
80
+ /**
81
+ * Gets the last client port which we know is safe from unexpected closes.
82
+ */
83
+ protected getLastWrappedPort(): Promise<WrappedSyncPort | undefined>;
84
+ /**
85
+ * In some very rare cases a specific tab might not respond to requests.
86
+ * This returns a random port which is not closing.
87
+ */
88
+ protected getRandomWrappedPort(): Promise<WrappedSyncPort | undefined>;
89
+ waitForStatus(status: SyncStatusOptions): Promise<void>;
90
+ waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
91
+ waitForReady(): Promise<void>;
92
+ private collectActiveSubscriptions;
93
+ updateSubscriptions(port: WrappedSyncPort, subscriptions: SubscribedStream[]): void;
94
+ setLogLevel(level: ILogLevel): void;
95
+ /**
96
+ * Configures the DBAdapter connection and a streaming sync client.
97
+ */
98
+ setParams(params: SharedSyncInitOptions): Promise<void>;
99
+ dispose(): Promise<void>;
100
+ /**
101
+ * Connects to the PowerSync backend instance.
102
+ * Multiple tabs can safely call this in their initialization.
103
+ * The connection will simply be reconnected whenever a new tab
104
+ * connects.
105
+ */
106
+ connect(options?: PowerSyncConnectionOptions): Promise<void>;
107
+ disconnect(): Promise<void>;
108
+ /**
109
+ * Adds a new client tab's message port to the list of connected ports
110
+ */
111
+ addPort(port: MessagePort): Promise<{
112
+ port: MessagePort;
113
+ clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
114
+ currentSubscriptions: never[];
115
+ closeListeners: never[];
116
+ isClosing: false;
117
+ }>;
118
+ /**
119
+ * Removes a message port client from this manager's managed
120
+ * clients.
121
+ */
122
+ removePort(port: WrappedSyncPort): Promise<() => void>;
123
+ triggerCrudUpload(): void;
124
+ hasCompletedSync(): Promise<boolean>;
125
+ getWriteCheckpoint(): Promise<string>;
126
+ protected withSyncImplementation<T>(callback: (sync: StreamingSyncImplementation) => Promise<T>): Promise<T>;
127
+ protected generateStreamingImplementation(): WebStreamingSyncImplementation;
128
+ /**
129
+ * Opens a worker wrapped database connection. Using the last connected client port.
130
+ */
131
+ protected openInternalDB(): Promise<WorkerWrappedAsyncDatabaseConnection<ResolvedWebSQLOpenOptions>>;
132
+ /**
133
+ * A method to update the all shared statuses for each
134
+ * client.
135
+ */
136
+ private updateAllStatuses;
137
+ }
@@ -0,0 +1,495 @@
1
+ import { AbortOperation, BaseObserver, ConnectionManager, SqliteBucketStorage, SyncStatus, createLogger } from '@powersync/common';
2
+ import { Mutex } from 'async-mutex';
3
+ import * as Comlink from 'comlink';
4
+ import { WebRemote } from '../../db/sync/WebRemote.js';
5
+ import { WebStreamingSyncImplementation } from '../../db/sync/WebStreamingSyncImplementation.js';
6
+ import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter.js';
7
+ import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection.js';
8
+ import { BroadcastLogger } from './BroadcastLogger.js';
9
+ /**
10
+ * @internal
11
+ * Manual message events for shared sync clients
12
+ */
13
+ export var SharedSyncClientEvent;
14
+ (function (SharedSyncClientEvent) {
15
+ /**
16
+ * This client requests the shared sync manager should
17
+ * close it's connection to the client.
18
+ */
19
+ SharedSyncClientEvent["CLOSE_CLIENT"] = "close-client";
20
+ SharedSyncClientEvent["CLOSE_ACK"] = "close-ack";
21
+ })(SharedSyncClientEvent || (SharedSyncClientEvent = {}));
22
+ /**
23
+ * HACK: The shared implementation wraps and provides its own
24
+ * PowerSyncBackendConnector when generating the streaming sync implementation.
25
+ * We provide this unused placeholder when connecting with the ConnectionManager.
26
+ */
27
+ const CONNECTOR_PLACEHOLDER = {};
28
+ /**
29
+ * @internal
30
+ * Shared sync implementation which runs inside a shared webworker
31
+ */
32
+ export class SharedSyncImplementation extends BaseObserver {
33
+ ports;
34
+ isInitialized;
35
+ statusListener;
36
+ fetchCredentialsController;
37
+ uploadDataController;
38
+ syncParams;
39
+ logger;
40
+ lastConnectOptions;
41
+ portMutex;
42
+ subscriptions = [];
43
+ connectionManager;
44
+ syncStatus;
45
+ broadCastLogger;
46
+ distributedDB;
47
+ constructor() {
48
+ super();
49
+ this.ports = [];
50
+ this.syncParams = null;
51
+ this.logger = createLogger('shared-sync');
52
+ this.lastConnectOptions = undefined;
53
+ this.portMutex = new Mutex();
54
+ this.isInitialized = new Promise((resolve) => {
55
+ const callback = this.registerListener({
56
+ initialized: () => {
57
+ resolve();
58
+ callback?.();
59
+ }
60
+ });
61
+ });
62
+ // Should be configured once we get params
63
+ this.distributedDB = null;
64
+ this.syncStatus = new SyncStatus({});
65
+ this.broadCastLogger = new BroadcastLogger(this.ports);
66
+ this.connectionManager = new ConnectionManager({
67
+ createSyncImplementation: async () => {
68
+ await this.waitForReady();
69
+ const sync = this.generateStreamingImplementation();
70
+ const onDispose = sync.registerListener({
71
+ statusChanged: (status) => {
72
+ this.updateAllStatuses(status.toJSON());
73
+ }
74
+ });
75
+ return {
76
+ sync,
77
+ onDispose
78
+ };
79
+ },
80
+ logger: this.logger
81
+ });
82
+ }
83
+ get lastSyncedAt() {
84
+ return this.connectionManager.syncStreamImplementation?.lastSyncedAt;
85
+ }
86
+ get isConnected() {
87
+ return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
88
+ }
89
+ /**
90
+ * Gets the last client port which we know is safe from unexpected closes.
91
+ */
92
+ async getLastWrappedPort() {
93
+ // Find the last port which is not closing
94
+ return await this.portMutex.runExclusive(() => {
95
+ for (let i = this.ports.length - 1; i >= 0; i--) {
96
+ if (!this.ports[i].isClosing) {
97
+ return this.ports[i];
98
+ }
99
+ }
100
+ return;
101
+ });
102
+ }
103
+ /**
104
+ * In some very rare cases a specific tab might not respond to requests.
105
+ * This returns a random port which is not closing.
106
+ */
107
+ async getRandomWrappedPort() {
108
+ return await this.portMutex.runExclusive(() => {
109
+ const nonClosingPorts = this.ports.filter((p) => !p.isClosing);
110
+ return nonClosingPorts[Math.floor(Math.random() * nonClosingPorts.length)];
111
+ });
112
+ }
113
+ async waitForStatus(status) {
114
+ return this.withSyncImplementation(async (sync) => {
115
+ return sync.waitForStatus(status);
116
+ });
117
+ }
118
+ async waitUntilStatusMatches(predicate) {
119
+ return this.withSyncImplementation(async (sync) => {
120
+ return sync.waitUntilStatusMatches(predicate);
121
+ });
122
+ }
123
+ async waitForReady() {
124
+ return this.isInitialized;
125
+ }
126
+ collectActiveSubscriptions() {
127
+ this.logger.debug('Collecting active stream subscriptions across tabs');
128
+ const active = new Map();
129
+ for (const port of this.ports) {
130
+ for (const stream of port.currentSubscriptions) {
131
+ const serializedKey = JSON.stringify(stream);
132
+ active.set(serializedKey, stream);
133
+ }
134
+ }
135
+ this.subscriptions = [...active.values()];
136
+ this.logger.debug('Collected stream subscriptions', this.subscriptions);
137
+ this.connectionManager.syncStreamImplementation?.updateSubscriptions(this.subscriptions);
138
+ }
139
+ updateSubscriptions(port, subscriptions) {
140
+ port.currentSubscriptions = subscriptions;
141
+ this.collectActiveSubscriptions();
142
+ }
143
+ setLogLevel(level) {
144
+ this.logger.setLevel(level);
145
+ this.broadCastLogger.setLevel(level);
146
+ }
147
+ /**
148
+ * Configures the DBAdapter connection and a streaming sync client.
149
+ */
150
+ async setParams(params) {
151
+ await this.portMutex.runExclusive(async () => {
152
+ this.collectActiveSubscriptions();
153
+ });
154
+ if (this.syncParams) {
155
+ // Cannot modify already existing sync implementation params
156
+ return;
157
+ }
158
+ // First time setting params
159
+ this.syncParams = params;
160
+ if (params.streamOptions?.flags?.broadcastLogs) {
161
+ this.logger = this.broadCastLogger;
162
+ }
163
+ const lockedAdapter = new LockedAsyncDatabaseAdapter({
164
+ name: params.dbParams.dbFilename,
165
+ openConnection: async () => {
166
+ // Gets a connection from the clients when a new connection is requested.
167
+ const db = await this.openInternalDB();
168
+ db.registerListener({
169
+ closing: () => {
170
+ lockedAdapter.reOpenInternalDB();
171
+ }
172
+ });
173
+ return db;
174
+ },
175
+ logger: this.logger,
176
+ reOpenOnConnectionClosed: true
177
+ });
178
+ this.distributedDB = lockedAdapter;
179
+ await lockedAdapter.init();
180
+ lockedAdapter.registerListener({
181
+ databaseReOpened: () => {
182
+ // We may have missed some table updates while the database was closed.
183
+ // We can poke the crud in case we missed any updates.
184
+ this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
185
+ /**
186
+ * FIXME or IMPROVE ME
187
+ * The Rust client implementation stores sync state on the connection level.
188
+ * Reopening the database causes a state machine error which should cause the
189
+ * StreamingSyncImplementation to reconnect. It would be nicer if we could trigger
190
+ * this reconnect earlier.
191
+ * This reconnect is not required for IndexedDB.
192
+ */
193
+ }
194
+ });
195
+ self.onerror = (event) => {
196
+ // Share any uncaught events on the broadcast logger
197
+ this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
198
+ };
199
+ this.iterateListeners((l) => l.initialized?.());
200
+ }
201
+ async dispose() {
202
+ await this.waitForReady();
203
+ this.statusListener?.();
204
+ return this.connectionManager.close();
205
+ }
206
+ /**
207
+ * Connects to the PowerSync backend instance.
208
+ * Multiple tabs can safely call this in their initialization.
209
+ * The connection will simply be reconnected whenever a new tab
210
+ * connects.
211
+ */
212
+ async connect(options) {
213
+ this.lastConnectOptions = options;
214
+ return this.connectionManager.connect(CONNECTOR_PLACEHOLDER, options ?? {});
215
+ }
216
+ async disconnect() {
217
+ return this.connectionManager.disconnect();
218
+ }
219
+ /**
220
+ * Adds a new client tab's message port to the list of connected ports
221
+ */
222
+ async addPort(port) {
223
+ return await this.portMutex.runExclusive(() => {
224
+ const portProvider = {
225
+ port,
226
+ clientProvider: Comlink.wrap(port),
227
+ currentSubscriptions: [],
228
+ closeListeners: [],
229
+ isClosing: false
230
+ };
231
+ this.ports.push(portProvider);
232
+ // Give the newly connected client the latest status
233
+ const status = this.connectionManager.syncStreamImplementation?.syncStatus;
234
+ if (status) {
235
+ portProvider.clientProvider.statusChanged(status.toJSON());
236
+ }
237
+ return portProvider;
238
+ });
239
+ }
240
+ /**
241
+ * Removes a message port client from this manager's managed
242
+ * clients.
243
+ */
244
+ async removePort(port) {
245
+ // Ports might be removed faster than we can process them.
246
+ port.isClosing = true;
247
+ // Remove the port within a mutex context.
248
+ // Warns if the port is not found. This should not happen in practice.
249
+ // We return early if the port is not found.
250
+ return await this.portMutex.runExclusive(async () => {
251
+ const index = this.ports.findIndex((p) => p == port);
252
+ if (index < 0) {
253
+ this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
254
+ return () => { };
255
+ }
256
+ const trackedPort = this.ports[index];
257
+ // Remove from the list of active ports
258
+ this.ports.splice(index, 1);
259
+ /**
260
+ * The port might currently be in use. Any active functions might
261
+ * not resolve. Abort them here.
262
+ */
263
+ [this.fetchCredentialsController, this.uploadDataController].forEach((abortController) => {
264
+ if (abortController?.activePort == port) {
265
+ abortController.controller.abort(new AbortOperation('Closing pending requests after client port is removed'));
266
+ }
267
+ });
268
+ // Close the worker wrapped database connection, we can't accurately rely on this connection
269
+ for (const closeListener of trackedPort.closeListeners) {
270
+ await closeListener();
271
+ }
272
+ this.collectActiveSubscriptions();
273
+ return () => trackedPort.clientProvider[Comlink.releaseProxy]();
274
+ });
275
+ }
276
+ triggerCrudUpload() {
277
+ this.withSyncImplementation(async (sync) => {
278
+ sync.triggerCrudUpload();
279
+ });
280
+ }
281
+ async hasCompletedSync() {
282
+ return this.withSyncImplementation(async (sync) => {
283
+ return sync.hasCompletedSync();
284
+ });
285
+ }
286
+ async getWriteCheckpoint() {
287
+ return this.withSyncImplementation(async (sync) => {
288
+ return sync.getWriteCheckpoint();
289
+ });
290
+ }
291
+ async withSyncImplementation(callback) {
292
+ await this.waitForReady();
293
+ if (this.connectionManager.syncStreamImplementation) {
294
+ return callback(this.connectionManager.syncStreamImplementation);
295
+ }
296
+ const sync = await new Promise((resolve) => {
297
+ const dispose = this.connectionManager.registerListener({
298
+ syncStreamCreated: (sync) => {
299
+ resolve(sync);
300
+ dispose?.();
301
+ }
302
+ });
303
+ });
304
+ return callback(sync);
305
+ }
306
+ generateStreamingImplementation() {
307
+ // This should only be called after initialization has completed
308
+ const syncParams = this.syncParams;
309
+ // Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
310
+ return new WebStreamingSyncImplementation({
311
+ adapter: new SqliteBucketStorage(this.distributedDB, this.logger),
312
+ remote: new WebRemote({
313
+ invalidateCredentials: async () => {
314
+ const lastPort = await this.getLastWrappedPort();
315
+ if (!lastPort) {
316
+ throw new Error('No client port found to invalidate credentials');
317
+ }
318
+ try {
319
+ this.logger.log('calling the last port client provider to invalidate credentials');
320
+ lastPort.clientProvider.invalidateCredentials();
321
+ }
322
+ catch (ex) {
323
+ this.logger.error('error invalidating credentials', ex);
324
+ }
325
+ },
326
+ fetchCredentials: async () => {
327
+ const lastPort = await this.getLastWrappedPort();
328
+ if (!lastPort) {
329
+ throw new Error('No client port found to fetch credentials');
330
+ }
331
+ return new Promise(async (resolve, reject) => {
332
+ const abortController = new AbortController();
333
+ this.fetchCredentialsController = {
334
+ controller: abortController,
335
+ activePort: lastPort
336
+ };
337
+ abortController.signal.onabort = reject;
338
+ try {
339
+ this.logger.log('calling the last port client provider for credentials');
340
+ resolve(await lastPort.clientProvider.fetchCredentials());
341
+ }
342
+ catch (ex) {
343
+ reject(ex);
344
+ }
345
+ finally {
346
+ this.fetchCredentialsController = undefined;
347
+ }
348
+ });
349
+ }
350
+ }, this.logger),
351
+ uploadCrud: async () => {
352
+ const lastPort = await this.getLastWrappedPort();
353
+ if (!lastPort) {
354
+ throw new Error('No client port found to upload crud');
355
+ }
356
+ return new Promise(async (resolve, reject) => {
357
+ const abortController = new AbortController();
358
+ this.uploadDataController = {
359
+ controller: abortController,
360
+ activePort: lastPort
361
+ };
362
+ // Resolving will make it retry
363
+ abortController.signal.onabort = () => resolve();
364
+ try {
365
+ resolve(await lastPort.clientProvider.uploadCrud());
366
+ }
367
+ catch (ex) {
368
+ reject(ex);
369
+ }
370
+ finally {
371
+ this.uploadDataController = undefined;
372
+ }
373
+ });
374
+ },
375
+ ...syncParams.streamOptions,
376
+ subscriptions: this.subscriptions,
377
+ // Logger cannot be transferred just yet
378
+ logger: this.logger
379
+ });
380
+ }
381
+ /**
382
+ * Opens a worker wrapped database connection. Using the last connected client port.
383
+ */
384
+ async openInternalDB() {
385
+ const client = await this.getRandomWrappedPort();
386
+ if (!client) {
387
+ // Should not really happen in practice
388
+ throw new Error(`Could not open DB connection since no client is connected.`);
389
+ }
390
+ // Fail-safe timeout for opening a database connection.
391
+ const timeout = setTimeout(() => {
392
+ abortController.abort();
393
+ }, 10_000);
394
+ /**
395
+ * Handle cases where the client might close while opening a connection.
396
+ */
397
+ const abortController = new AbortController();
398
+ const closeListener = () => {
399
+ abortController.abort();
400
+ };
401
+ const removeCloseListener = () => {
402
+ const index = client.closeListeners.indexOf(closeListener);
403
+ if (index >= 0) {
404
+ client.closeListeners.splice(index, 1);
405
+ }
406
+ };
407
+ client.closeListeners.push(closeListener);
408
+ const workerPort = await withAbort({
409
+ action: () => client.clientProvider.getDBWorkerPort(),
410
+ signal: abortController.signal,
411
+ cleanupOnAbort: (port) => {
412
+ port.close();
413
+ }
414
+ }).catch((ex) => {
415
+ removeCloseListener();
416
+ throw ex;
417
+ });
418
+ const remote = Comlink.wrap(workerPort);
419
+ const identifier = this.syncParams.dbParams.dbFilename;
420
+ /**
421
+ * The open could fail if the tab is closed while we're busy opening the database.
422
+ * This operation is typically executed inside an exclusive portMutex lock.
423
+ * We typically execute the closeListeners using the portMutex in a different context.
424
+ * We can't rely on the closeListeners to abort the operation if the tab is closed.
425
+ */
426
+ const db = await withAbort({
427
+ action: () => remote(this.syncParams.dbParams),
428
+ signal: abortController.signal,
429
+ cleanupOnAbort: (db) => {
430
+ db.close();
431
+ }
432
+ }).finally(() => {
433
+ // We can remove the close listener here since we no longer need it past this point.
434
+ removeCloseListener();
435
+ });
436
+ clearTimeout(timeout);
437
+ const wrapped = new WorkerWrappedAsyncDatabaseConnection({
438
+ remote,
439
+ baseConnection: db,
440
+ identifier,
441
+ // It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
442
+ // that and ensure pending requests are aborted when the tab is closed.
443
+ remoteCanCloseUnexpectedly: true
444
+ });
445
+ client.closeListeners.push(async () => {
446
+ this.logger.info('Aborting open connection because associated tab closed.');
447
+ /**
448
+ * Don't await this close operation. It might never resolve if the tab is closed.
449
+ * We mark the remote as closed first, this will reject any pending requests.
450
+ * We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
451
+ */
452
+ wrapped.markRemoteClosed();
453
+ wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
454
+ });
455
+ return wrapped;
456
+ }
457
+ /**
458
+ * A method to update the all shared statuses for each
459
+ * client.
460
+ */
461
+ updateAllStatuses(status) {
462
+ this.syncStatus = new SyncStatus(status);
463
+ this.ports.forEach((p) => p.clientProvider.statusChanged(status));
464
+ }
465
+ }
466
+ /**
467
+ * Runs the action with an abort controller.
468
+ */
469
+ function withAbort(options) {
470
+ const { action, signal, cleanupOnAbort } = options;
471
+ return new Promise((resolve, reject) => {
472
+ if (signal.aborted) {
473
+ reject(new AbortOperation('Operation aborted by abort controller'));
474
+ return;
475
+ }
476
+ function handleAbort() {
477
+ signal.removeEventListener('abort', handleAbort);
478
+ reject(new AbortOperation('Operation aborted by abort controller'));
479
+ }
480
+ signal.addEventListener('abort', handleAbort, { once: true });
481
+ function completePromise(action) {
482
+ signal.removeEventListener('abort', handleAbort);
483
+ action();
484
+ }
485
+ action()
486
+ .then((data) => {
487
+ // We already rejected due to the abort, allow for cleanup
488
+ if (signal.aborted) {
489
+ return completePromise(() => cleanupOnAbort?.(data));
490
+ }
491
+ completePromise(() => resolve(data));
492
+ })
493
+ .catch((e) => completePromise(() => reject(e)));
494
+ });
495
+ }
@@ -0,0 +1,11 @@
1
+ import { createBaseLogger } from '@powersync/common';
2
+ import { SharedSyncImplementation } from './SharedSyncImplementation.js';
3
+ import { WorkerClient } from './WorkerClient.js';
4
+ const _self = self;
5
+ const logger = createBaseLogger();
6
+ logger.useDefaults();
7
+ const sharedSyncImplementation = new SharedSyncImplementation();
8
+ _self.onconnect = async function (event) {
9
+ const port = event.ports[0];
10
+ new WorkerClient(sharedSyncImplementation, port);
11
+ };
@@ -0,0 +1,31 @@
1
+ import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream } from '@powersync/common';
2
+ import { SharedSyncImplementation, SharedSyncInitOptions, WrappedSyncPort } from './SharedSyncImplementation.js';
3
+ /**
4
+ * A client to the shared sync worker.
5
+ *
6
+ * The shared sync implementation needs a per-client view of subscriptions so that subscriptions of closed tabs can
7
+ * automatically be evicted later.
8
+ */
9
+ export declare class WorkerClient {
10
+ private readonly sync;
11
+ private readonly port;
12
+ private resolvedPort;
13
+ protected resolvedPortPromise: Promise<WrappedSyncPort> | null;
14
+ constructor(sync: SharedSyncImplementation, port: MessagePort);
15
+ private removePort;
16
+ /**
17
+ * Called by a client after obtaining a lock with a random name.
18
+ *
19
+ * When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
20
+ * it can consider the connection to be closed.
21
+ */
22
+ addLockBasedCloseSignal(name: string): Promise<void>;
23
+ setLogLevel(level: ILogLevel): void;
24
+ triggerCrudUpload(): void;
25
+ setParams(params: SharedSyncInitOptions, subscriptions: SubscribedStream[]): Promise<void>;
26
+ getWriteCheckpoint(): Promise<string>;
27
+ hasCompletedSync(): Promise<boolean>;
28
+ connect(options?: PowerSyncConnectionOptions): Promise<void>;
29
+ updateSubscriptions(subscriptions: SubscribedStream[]): void;
30
+ disconnect(): Promise<void>;
31
+ }