@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.
- package/dist/0b19af1befc07ce338dd.wasm +0 -0
- package/dist/2632c3bda9473da74fd5.wasm +0 -0
- package/dist/64f5351ba3784bfe2f3e.wasm +0 -0
- package/dist/9318ca94aac4d0fe0135.wasm +0 -0
- package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js +1878 -0
- package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js.map +1 -0
- package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530150.index.umd.js +555 -0
- package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530150.index.umd.js.map +1 -0
- package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530151.index.umd.js +555 -0
- package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530151.index.umd.js.map +1 -0
- package/dist/index.umd.js +8267 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/worker/SharedSyncImplementation.umd.js +19059 -0
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -0
- package/dist/worker/WASQLiteDB.umd.js +17736 -0
- package/dist/worker/WASQLiteDB.umd.js.map +1 -0
- package/dist/worker/node_modules_pnpm_bson_6_10_4_node_modules_bson_lib_bson_mjs.umd.js +4646 -0
- package/dist/worker/node_modules_pnpm_bson_6_10_4_node_modules_bson_lib_bson_mjs.umd.js.map +1 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-0d2437.umd.js +2478 -0
- 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
- package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-1d4e74.umd.js +1820 -0
- 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
- package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-3622cf.umd.js +1681 -0
- 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
- package/lib/package.json +95 -0
- package/lib/src/attachments/IndexDBFileSystemAdapter.d.ts +25 -0
- package/lib/src/attachments/IndexDBFileSystemAdapter.js +104 -0
- package/lib/src/db/NavigatorTriggerClaimManager.d.ts +6 -0
- package/lib/src/db/NavigatorTriggerClaimManager.js +20 -0
- package/lib/src/db/PowerSyncDatabase.d.ts +79 -0
- package/lib/src/db/PowerSyncDatabase.js +166 -0
- package/lib/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.d.ts +23 -0
- package/lib/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.js +26 -0
- package/lib/src/db/adapters/AbstractWebSQLOpenFactory.d.ts +17 -0
- package/lib/src/db/adapters/AbstractWebSQLOpenFactory.js +33 -0
- package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +49 -0
- package/lib/src/db/adapters/AsyncDatabaseConnection.js +1 -0
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +109 -0
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +401 -0
- package/lib/src/db/adapters/SSRDBAdapter.d.ts +31 -0
- package/lib/src/db/adapters/SSRDBAdapter.js +69 -0
- package/lib/src/db/adapters/WebDBAdapter.d.ts +20 -0
- package/lib/src/db/adapters/WebDBAdapter.js +1 -0
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +59 -0
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +147 -0
- package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.d.ts +12 -0
- package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.js +19 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +155 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +401 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.d.ts +32 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +49 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +23 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +96 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.d.ts +15 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.js +21 -0
- package/lib/src/db/adapters/web-sql-flags.d.ts +87 -0
- package/lib/src/db/adapters/web-sql-flags.js +36 -0
- package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +48 -0
- package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +65 -0
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +57 -0
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +237 -0
- package/lib/src/db/sync/WebRemote.d.ts +9 -0
- package/lib/src/db/sync/WebRemote.js +44 -0
- package/lib/src/db/sync/WebStreamingSyncImplementation.d.ts +13 -0
- package/lib/src/db/sync/WebStreamingSyncImplementation.js +18 -0
- package/lib/src/db/sync/userAgent.d.ts +17 -0
- package/lib/src/db/sync/userAgent.js +64 -0
- package/lib/src/index.d.ts +15 -0
- package/lib/src/index.js +15 -0
- package/lib/src/shared/navigator.d.ts +1 -0
- package/lib/src/shared/navigator.js +6 -0
- package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +42 -0
- package/lib/src/worker/db/SharedWASQLiteConnection.js +90 -0
- package/lib/src/worker/db/WASQLiteDB.worker.d.ts +4 -0
- package/lib/src/worker/db/WASQLiteDB.worker.js +69 -0
- package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +9 -0
- package/lib/src/worker/db/WorkerWASQLiteConnection.js +12 -0
- package/lib/src/worker/db/open-worker-database.d.ts +14 -0
- package/lib/src/worker/db/open-worker-database.js +52 -0
- package/lib/src/worker/sync/AbstractSharedSyncClientProvider.d.ts +19 -0
- package/lib/src/worker/sync/AbstractSharedSyncClientProvider.js +5 -0
- package/lib/src/worker/sync/BroadcastLogger.d.ts +47 -0
- package/lib/src/worker/sync/BroadcastLogger.js +128 -0
- package/lib/src/worker/sync/SharedSyncImplementation.d.ts +137 -0
- package/lib/src/worker/sync/SharedSyncImplementation.js +495 -0
- package/lib/src/worker/sync/SharedSyncImplementation.worker.d.ts +1 -0
- package/lib/src/worker/sync/SharedSyncImplementation.worker.js +11 -0
- package/lib/src/worker/sync/WorkerClient.d.ts +31 -0
- package/lib/src/worker/sync/WorkerClient.js +84 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +3 -3
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|