@powersync/common 1.41.0 → 1.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.cjs +10820 -22
- package/dist/bundle.cjs.map +1 -0
- package/dist/bundle.mjs +10741 -22
- package/dist/bundle.mjs.map +1 -0
- package/dist/bundle.node.cjs +10820 -0
- package/dist/bundle.node.cjs.map +1 -0
- package/dist/bundle.node.mjs +10741 -0
- package/dist/bundle.node.mjs.map +1 -0
- package/dist/index.d.cts +77 -13
- package/lib/client/AbstractPowerSyncDatabase.js +1 -0
- package/lib/client/AbstractPowerSyncDatabase.js.map +1 -0
- package/lib/client/AbstractPowerSyncOpenFactory.js +1 -0
- package/lib/client/AbstractPowerSyncOpenFactory.js.map +1 -0
- package/lib/client/ConnectionManager.js +1 -0
- package/lib/client/ConnectionManager.js.map +1 -0
- package/lib/client/CustomQuery.js +1 -0
- package/lib/client/CustomQuery.js.map +1 -0
- package/lib/client/Query.js +1 -0
- package/lib/client/Query.js.map +1 -0
- package/lib/client/SQLOpenFactory.js +1 -0
- package/lib/client/SQLOpenFactory.js.map +1 -0
- package/lib/client/compilableQueryWatch.js +1 -0
- package/lib/client/compilableQueryWatch.js.map +1 -0
- package/lib/client/connection/PowerSyncBackendConnector.js +1 -0
- package/lib/client/connection/PowerSyncBackendConnector.js.map +1 -0
- package/lib/client/connection/PowerSyncCredentials.js +1 -0
- package/lib/client/connection/PowerSyncCredentials.js.map +1 -0
- package/lib/client/constants.js +1 -0
- package/lib/client/constants.js.map +1 -0
- package/lib/client/runOnSchemaChange.js +1 -0
- package/lib/client/runOnSchemaChange.js.map +1 -0
- package/lib/client/sync/bucket/BucketStorageAdapter.js +1 -0
- package/lib/client/sync/bucket/BucketStorageAdapter.js.map +1 -0
- package/lib/client/sync/bucket/CrudBatch.js +1 -0
- package/lib/client/sync/bucket/CrudBatch.js.map +1 -0
- package/lib/client/sync/bucket/CrudEntry.js +1 -0
- package/lib/client/sync/bucket/CrudEntry.js.map +1 -0
- package/lib/client/sync/bucket/CrudTransaction.js +1 -0
- package/lib/client/sync/bucket/CrudTransaction.js.map +1 -0
- package/lib/client/sync/bucket/OpType.js +1 -0
- package/lib/client/sync/bucket/OpType.js.map +1 -0
- package/lib/client/sync/bucket/OplogEntry.js +1 -0
- package/lib/client/sync/bucket/OplogEntry.js.map +1 -0
- package/lib/client/sync/bucket/SqliteBucketStorage.js +1 -0
- package/lib/client/sync/bucket/SqliteBucketStorage.js.map +1 -0
- package/lib/client/sync/bucket/SyncDataBatch.js +1 -0
- package/lib/client/sync/bucket/SyncDataBatch.js.map +1 -0
- package/lib/client/sync/bucket/SyncDataBucket.js +1 -0
- package/lib/client/sync/bucket/SyncDataBucket.js.map +1 -0
- package/lib/client/sync/stream/AbstractRemote.d.ts +5 -0
- package/lib/client/sync/stream/AbstractRemote.js +19 -6
- package/lib/client/sync/stream/AbstractRemote.js.map +1 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +1 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -0
- package/lib/client/sync/stream/WebsocketClientTransport.js +1 -0
- package/lib/client/sync/stream/WebsocketClientTransport.js.map +1 -0
- package/lib/client/sync/stream/core-instruction.js +1 -0
- package/lib/client/sync/stream/core-instruction.js.map +1 -0
- package/lib/client/sync/stream/streaming-sync-types.js +1 -0
- package/lib/client/sync/stream/streaming-sync-types.js.map +1 -0
- package/lib/client/sync/sync-streams.js +1 -0
- package/lib/client/sync/sync-streams.js.map +1 -0
- package/lib/client/triggers/TriggerManager.d.ts +71 -12
- package/lib/client/triggers/TriggerManager.js +1 -0
- package/lib/client/triggers/TriggerManager.js.map +1 -0
- package/lib/client/triggers/TriggerManagerImpl.js +11 -5
- package/lib/client/triggers/TriggerManagerImpl.js.map +1 -0
- package/lib/client/triggers/sanitizeSQL.js +1 -0
- package/lib/client/triggers/sanitizeSQL.js.map +1 -0
- package/lib/client/watched/GetAllQuery.js +1 -0
- package/lib/client/watched/GetAllQuery.js.map +1 -0
- package/lib/client/watched/WatchedQuery.js +1 -0
- package/lib/client/watched/WatchedQuery.js.map +1 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.js +1 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.js.map +1 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.js +1 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.js.map +1 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.js +1 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.js.map +1 -0
- package/lib/client/watched/processors/comparators.js +1 -0
- package/lib/client/watched/processors/comparators.js.map +1 -0
- package/lib/db/DBAdapter.js +1 -0
- package/lib/db/DBAdapter.js.map +1 -0
- package/lib/db/crud/SyncProgress.js +1 -0
- package/lib/db/crud/SyncProgress.js.map +1 -0
- package/lib/db/crud/SyncStatus.js +1 -0
- package/lib/db/crud/SyncStatus.js.map +1 -0
- package/lib/db/crud/UploadQueueStatus.js +1 -0
- package/lib/db/crud/UploadQueueStatus.js.map +1 -0
- package/lib/db/schema/Column.js +1 -0
- package/lib/db/schema/Column.js.map +1 -0
- package/lib/db/schema/Index.js +1 -0
- package/lib/db/schema/Index.js.map +1 -0
- package/lib/db/schema/IndexedColumn.js +1 -0
- package/lib/db/schema/IndexedColumn.js.map +1 -0
- package/lib/db/schema/RawTable.js +1 -0
- package/lib/db/schema/RawTable.js.map +1 -0
- package/lib/db/schema/Schema.js +1 -0
- package/lib/db/schema/Schema.js.map +1 -0
- package/lib/db/schema/Table.js +1 -0
- package/lib/db/schema/Table.js.map +1 -0
- package/lib/db/schema/TableV2.js +1 -0
- package/lib/db/schema/TableV2.js.map +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -0
- package/lib/types/types.js +1 -0
- package/lib/types/types.js.map +1 -0
- package/lib/utils/AbortOperation.js +1 -0
- package/lib/utils/AbortOperation.js.map +1 -0
- package/lib/utils/BaseObserver.js +1 -0
- package/lib/utils/BaseObserver.js.map +1 -0
- package/lib/utils/ControlledExecutor.js +1 -0
- package/lib/utils/ControlledExecutor.js.map +1 -0
- package/lib/utils/DataStream.js +1 -0
- package/lib/utils/DataStream.js.map +1 -0
- package/lib/utils/Logger.js +1 -0
- package/lib/utils/Logger.js.map +1 -0
- package/lib/utils/MetaBaseObserver.js +1 -0
- package/lib/utils/MetaBaseObserver.js.map +1 -0
- package/lib/utils/async.js +1 -0
- package/lib/utils/async.js.map +1 -0
- package/lib/utils/mutex.js +1 -0
- package/lib/utils/mutex.js.map +1 -0
- package/lib/utils/parseQuery.js +1 -0
- package/lib/utils/parseQuery.js.map +1 -0
- package/package.json +23 -15
- package/src/client/AbstractPowerSyncDatabase.ts +1343 -0
- package/src/client/AbstractPowerSyncOpenFactory.ts +39 -0
- package/src/client/ConnectionManager.ts +402 -0
- package/src/client/CustomQuery.ts +56 -0
- package/src/client/Query.ts +106 -0
- package/src/client/SQLOpenFactory.ts +55 -0
- package/src/client/compilableQueryWatch.ts +55 -0
- package/src/client/connection/PowerSyncBackendConnector.ts +25 -0
- package/src/client/connection/PowerSyncCredentials.ts +5 -0
- package/src/client/constants.ts +1 -0
- package/src/client/runOnSchemaChange.ts +31 -0
- package/src/client/sync/bucket/BucketStorageAdapter.ts +118 -0
- package/src/client/sync/bucket/CrudBatch.ts +21 -0
- package/src/client/sync/bucket/CrudEntry.ts +172 -0
- package/src/client/sync/bucket/CrudTransaction.ts +21 -0
- package/src/client/sync/bucket/OpType.ts +23 -0
- package/src/client/sync/bucket/OplogEntry.ts +50 -0
- package/src/client/sync/bucket/SqliteBucketStorage.ts +395 -0
- package/src/client/sync/bucket/SyncDataBatch.ts +11 -0
- package/src/client/sync/bucket/SyncDataBucket.ts +49 -0
- package/src/client/sync/stream/AbstractRemote.ts +636 -0
- package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +1258 -0
- package/src/client/sync/stream/WebsocketClientTransport.ts +80 -0
- package/src/client/sync/stream/core-instruction.ts +99 -0
- package/src/client/sync/stream/streaming-sync-types.ts +205 -0
- package/src/client/sync/sync-streams.ts +107 -0
- package/src/client/triggers/TriggerManager.ts +451 -0
- package/src/client/triggers/TriggerManagerImpl.ts +320 -0
- package/src/client/triggers/sanitizeSQL.ts +66 -0
- package/src/client/watched/GetAllQuery.ts +46 -0
- package/src/client/watched/WatchedQuery.ts +121 -0
- package/src/client/watched/processors/AbstractQueryProcessor.ts +226 -0
- package/src/client/watched/processors/DifferentialQueryProcessor.ts +305 -0
- package/src/client/watched/processors/OnChangeQueryProcessor.ts +122 -0
- package/src/client/watched/processors/comparators.ts +57 -0
- package/src/db/DBAdapter.ts +134 -0
- package/src/db/crud/SyncProgress.ts +100 -0
- package/src/db/crud/SyncStatus.ts +308 -0
- package/src/db/crud/UploadQueueStatus.ts +20 -0
- package/src/db/schema/Column.ts +60 -0
- package/src/db/schema/Index.ts +39 -0
- package/src/db/schema/IndexedColumn.ts +42 -0
- package/src/db/schema/RawTable.ts +67 -0
- package/src/db/schema/Schema.ts +76 -0
- package/src/db/schema/Table.ts +359 -0
- package/src/db/schema/TableV2.ts +9 -0
- package/src/index.ts +52 -0
- package/src/types/types.ts +9 -0
- package/src/utils/AbortOperation.ts +17 -0
- package/src/utils/BaseObserver.ts +41 -0
- package/src/utils/ControlledExecutor.ts +72 -0
- package/src/utils/DataStream.ts +211 -0
- package/src/utils/Logger.ts +47 -0
- package/src/utils/MetaBaseObserver.ts +81 -0
- package/src/utils/async.ts +61 -0
- package/src/utils/mutex.ts +34 -0
- package/src/utils/parseQuery.ts +25 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import type { BSON } from 'bson';
|
|
2
|
+
import { type fetch } from 'cross-fetch';
|
|
3
|
+
import Logger, { ILogger } from 'js-logger';
|
|
4
|
+
import { RSocket, RSocketConnector, Requestable } from 'rsocket-core';
|
|
5
|
+
import PACKAGE from '../../../../package.json' with { type: 'json' };
|
|
6
|
+
import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
7
|
+
import { DataStream } from '../../../utils/DataStream.js';
|
|
8
|
+
import { PowerSyncCredentials } from '../../connection/PowerSyncCredentials.js';
|
|
9
|
+
import { WebsocketClientTransport } from './WebsocketClientTransport.js';
|
|
10
|
+
import { StreamingSyncRequest } from './streaming-sync-types.js';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export type BSONImplementation = typeof BSON;
|
|
14
|
+
|
|
15
|
+
export type RemoteConnector = {
|
|
16
|
+
fetchCredentials: () => Promise<PowerSyncCredentials | null>;
|
|
17
|
+
invalidateCredentials?: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
|
|
21
|
+
const POWERSYNC_JS_VERSION = PACKAGE.version;
|
|
22
|
+
|
|
23
|
+
const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
|
|
24
|
+
|
|
25
|
+
// Keep alive message is sent every period
|
|
26
|
+
const KEEP_ALIVE_MS = 20_000;
|
|
27
|
+
|
|
28
|
+
// One message of any type must be received in this period.
|
|
29
|
+
const SOCKET_TIMEOUT_MS = 30_000;
|
|
30
|
+
|
|
31
|
+
// One keepalive message must be received in this period.
|
|
32
|
+
// If there is a backlog of messages (for example on slow connections), keepalive messages could be delayed
|
|
33
|
+
// significantly. Therefore this is longer than the socket timeout.
|
|
34
|
+
const KEEP_ALIVE_LIFETIME_MS = 90_000;
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_REMOTE_LOGGER = Logger.get('PowerSyncRemote');
|
|
37
|
+
|
|
38
|
+
export type SyncStreamOptions = {
|
|
39
|
+
path: string;
|
|
40
|
+
data: StreamingSyncRequest;
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
abortSignal?: AbortSignal;
|
|
43
|
+
fetchOptions?: Request;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export enum FetchStrategy {
|
|
47
|
+
/**
|
|
48
|
+
* Queues multiple sync events before processing, reducing round-trips.
|
|
49
|
+
* This comes at the cost of more processing overhead, which may cause ACK timeouts on older/weaker devices for big enough datasets.
|
|
50
|
+
*/
|
|
51
|
+
Buffered = 'buffered',
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Processes each sync event immediately before requesting the next.
|
|
55
|
+
* This reduces processing overhead and improves real-time responsiveness.
|
|
56
|
+
*/
|
|
57
|
+
Sequential = 'sequential'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type SocketSyncStreamOptions = SyncStreamOptions & {
|
|
61
|
+
fetchStrategy: FetchStrategy;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type FetchImplementation = typeof fetch;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Class wrapper for providing a fetch implementation.
|
|
68
|
+
* The class wrapper is used to distinguish the fetchImplementation
|
|
69
|
+
* option in [AbstractRemoteOptions] from the general fetch method
|
|
70
|
+
* which is typeof "function"
|
|
71
|
+
*/
|
|
72
|
+
export class FetchImplementationProvider {
|
|
73
|
+
getFetch(): FetchImplementation {
|
|
74
|
+
throw new Error('Unspecified fetch implementation');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type AbstractRemoteOptions = {
|
|
79
|
+
/**
|
|
80
|
+
* Transforms the PowerSync base URL which might contain
|
|
81
|
+
* `http(s)://` to the corresponding WebSocket variant
|
|
82
|
+
* e.g. `ws(s)://`
|
|
83
|
+
*/
|
|
84
|
+
socketUrlTransformer: (url: string) => string;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Optionally provide the fetch implementation to use.
|
|
88
|
+
* Note that this usually needs to be bound to the global scope.
|
|
89
|
+
* Binding should be done before passing here.
|
|
90
|
+
*/
|
|
91
|
+
fetchImplementation: FetchImplementation | FetchImplementationProvider;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Optional options to pass directly to all `fetch` calls.
|
|
95
|
+
*
|
|
96
|
+
* This can include fields such as `dispatcher` (e.g. for proxy support),
|
|
97
|
+
* `cache`, or any other fetch-compatible options.
|
|
98
|
+
*/
|
|
99
|
+
fetchOptions?: {};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const DEFAULT_REMOTE_OPTIONS: AbstractRemoteOptions = {
|
|
103
|
+
socketUrlTransformer: (url) =>
|
|
104
|
+
url.replace(/^https?:\/\//, function (match) {
|
|
105
|
+
return match === 'https://' ? 'wss://' : 'ws://';
|
|
106
|
+
}),
|
|
107
|
+
fetchImplementation: new FetchImplementationProvider(),
|
|
108
|
+
fetchOptions: {}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export abstract class AbstractRemote {
|
|
112
|
+
protected credentials: PowerSyncCredentials | null = null;
|
|
113
|
+
protected options: AbstractRemoteOptions;
|
|
114
|
+
|
|
115
|
+
constructor(
|
|
116
|
+
protected connector: RemoteConnector,
|
|
117
|
+
protected logger: ILogger = DEFAULT_REMOTE_LOGGER,
|
|
118
|
+
options?: Partial<AbstractRemoteOptions>
|
|
119
|
+
) {
|
|
120
|
+
this.options = {
|
|
121
|
+
...DEFAULT_REMOTE_OPTIONS,
|
|
122
|
+
...(options ?? {})
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @returns a fetch implementation (function)
|
|
128
|
+
* which can be called to perform fetch requests
|
|
129
|
+
*/
|
|
130
|
+
get fetch(): FetchImplementation {
|
|
131
|
+
const { fetchImplementation } = this.options;
|
|
132
|
+
return fetchImplementation instanceof FetchImplementationProvider
|
|
133
|
+
? fetchImplementation.getFetch()
|
|
134
|
+
: fetchImplementation;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get credentials currently cached, or fetch new credentials if none are
|
|
139
|
+
* available.
|
|
140
|
+
*
|
|
141
|
+
* These credentials may have expired already.
|
|
142
|
+
*/
|
|
143
|
+
async getCredentials(): Promise<PowerSyncCredentials | null> {
|
|
144
|
+
if (this.credentials) {
|
|
145
|
+
return this.credentials;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return this.prefetchCredentials();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Fetch a new set of credentials and cache it.
|
|
153
|
+
*
|
|
154
|
+
* Until this call succeeds, `getCredentials` will still return the
|
|
155
|
+
* old credentials.
|
|
156
|
+
*
|
|
157
|
+
* This may be called before the current credentials have expired.
|
|
158
|
+
*/
|
|
159
|
+
async prefetchCredentials() {
|
|
160
|
+
this.credentials = await this.fetchCredentials();
|
|
161
|
+
|
|
162
|
+
return this.credentials;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get credentials for PowerSync.
|
|
167
|
+
*
|
|
168
|
+
* This should always fetch a fresh set of credentials - don't use cached
|
|
169
|
+
* values.
|
|
170
|
+
*/
|
|
171
|
+
async fetchCredentials() {
|
|
172
|
+
const credentials = await this.connector.fetchCredentials();
|
|
173
|
+
if (credentials?.endpoint.match(POWERSYNC_TRAILING_SLASH_MATCH)) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`A trailing forward slash "/" was found in the fetchCredentials endpoint: "${credentials.endpoint}". Remove the trailing forward slash "/" to fix this error.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return credentials;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/***
|
|
183
|
+
* Immediately invalidate credentials.
|
|
184
|
+
*
|
|
185
|
+
* This may be called when the current credentials have expired.
|
|
186
|
+
*/
|
|
187
|
+
invalidateCredentials() {
|
|
188
|
+
this.credentials = null;
|
|
189
|
+
this.connector.invalidateCredentials?.();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
getUserAgent() {
|
|
193
|
+
return `powersync-js/${POWERSYNC_JS_VERSION}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
protected async buildRequest(path: string) {
|
|
197
|
+
const credentials = await this.getCredentials();
|
|
198
|
+
if (credentials != null && (credentials.endpoint == null || credentials.endpoint == '')) {
|
|
199
|
+
throw new Error('PowerSync endpoint not configured');
|
|
200
|
+
} else if (credentials?.token == null || credentials?.token == '') {
|
|
201
|
+
const error: any = new Error(`Not signed in`);
|
|
202
|
+
error.status = 401;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const userAgent = this.getUserAgent();
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
url: credentials.endpoint + path,
|
|
210
|
+
headers: {
|
|
211
|
+
'content-type': 'application/json',
|
|
212
|
+
Authorization: `Token ${credentials.token}`,
|
|
213
|
+
'x-user-agent': userAgent
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async post(path: string, data: any, headers: Record<string, string> = {}): Promise<any> {
|
|
219
|
+
const request = await this.buildRequest(path);
|
|
220
|
+
const res = await this.fetch(request.url, {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: {
|
|
223
|
+
...headers,
|
|
224
|
+
...request.headers
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify(data)
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (res.status === 401) {
|
|
230
|
+
this.invalidateCredentials();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!res.ok) {
|
|
234
|
+
throw new Error(`Received ${res.status} - ${res.statusText} when posting to ${path}: ${await res.text()}}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return res.json();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async get(path: string, headers?: Record<string, string>): Promise<any> {
|
|
241
|
+
const request = await this.buildRequest(path);
|
|
242
|
+
const res = await this.fetch(request.url, {
|
|
243
|
+
method: 'GET',
|
|
244
|
+
headers: {
|
|
245
|
+
...headers,
|
|
246
|
+
...request.headers
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (res.status === 401) {
|
|
251
|
+
this.invalidateCredentials();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
throw new Error(`Received ${res.status} - ${res.statusText} when getting from ${path}: ${await res.text()}}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return res.json();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Provides a BSON implementation. The import nature of this varies depending on the platform
|
|
263
|
+
*/
|
|
264
|
+
abstract getBSON(): Promise<BSONImplementation>;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @returns A text decoder decoding UTF-8. This is a method to allow patching it for Hermes which doesn't support the
|
|
268
|
+
* builtin, without forcing us to bundle a polyfill with `@powersync/common`.
|
|
269
|
+
*/
|
|
270
|
+
protected createTextDecoder(): TextDecoder {
|
|
271
|
+
return new TextDecoder();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
protected createSocket(url: string): WebSocket {
|
|
275
|
+
return new WebSocket(url);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Returns a data stream of sync line data.
|
|
280
|
+
*
|
|
281
|
+
* @param map Maps received payload frames to the typed event value.
|
|
282
|
+
* @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
|
|
283
|
+
* (required for compatibility with older sync services).
|
|
284
|
+
*/
|
|
285
|
+
async socketStreamRaw<T>(
|
|
286
|
+
options: SocketSyncStreamOptions,
|
|
287
|
+
map: (buffer: Uint8Array) => T,
|
|
288
|
+
bson?: typeof BSON
|
|
289
|
+
): Promise<DataStream<T>> {
|
|
290
|
+
const { path, fetchStrategy = FetchStrategy.Buffered } = options;
|
|
291
|
+
const mimeType = bson == null ? 'application/json' : 'application/bson';
|
|
292
|
+
|
|
293
|
+
function toBuffer(js: any): Buffer {
|
|
294
|
+
let contents: any;
|
|
295
|
+
if (bson != null) {
|
|
296
|
+
contents = bson.serialize(js);
|
|
297
|
+
} else {
|
|
298
|
+
contents = JSON.stringify(js);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return Buffer.from(contents);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const syncQueueRequestSize = fetchStrategy == FetchStrategy.Buffered ? 10 : 1;
|
|
305
|
+
const request = await this.buildRequest(path);
|
|
306
|
+
|
|
307
|
+
// Add the user agent in the setup payload - we can't set custom
|
|
308
|
+
// headers with websockets on web. The browser userAgent is however added
|
|
309
|
+
// automatically as a header.
|
|
310
|
+
const userAgent = this.getUserAgent();
|
|
311
|
+
|
|
312
|
+
const stream = new DataStream<T, Uint8Array>({
|
|
313
|
+
logger: this.logger,
|
|
314
|
+
pressure: {
|
|
315
|
+
lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
|
|
316
|
+
},
|
|
317
|
+
mapLine: map
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Handle upstream abort
|
|
321
|
+
if (options.abortSignal?.aborted) {
|
|
322
|
+
throw new AbortOperation('Connection request aborted');
|
|
323
|
+
} else {
|
|
324
|
+
options.abortSignal?.addEventListener(
|
|
325
|
+
'abort',
|
|
326
|
+
() => {
|
|
327
|
+
stream.close();
|
|
328
|
+
},
|
|
329
|
+
{ once: true }
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let keepAliveTimeout: any;
|
|
334
|
+
const resetTimeout = () => {
|
|
335
|
+
clearTimeout(keepAliveTimeout);
|
|
336
|
+
keepAliveTimeout = setTimeout(() => {
|
|
337
|
+
this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
|
|
338
|
+
stream.close();
|
|
339
|
+
}, SOCKET_TIMEOUT_MS);
|
|
340
|
+
};
|
|
341
|
+
resetTimeout();
|
|
342
|
+
|
|
343
|
+
// Typescript complains about this being `never` if it's not assigned here.
|
|
344
|
+
// This is assigned in `wsCreator`.
|
|
345
|
+
let disposeSocketConnectionTimeout = () => {};
|
|
346
|
+
|
|
347
|
+
const url = this.options.socketUrlTransformer(request.url);
|
|
348
|
+
const connector = new RSocketConnector({
|
|
349
|
+
transport: new WebsocketClientTransport({
|
|
350
|
+
url,
|
|
351
|
+
wsCreator: (url) => {
|
|
352
|
+
const socket = this.createSocket(url);
|
|
353
|
+
disposeSocketConnectionTimeout = stream.registerListener({
|
|
354
|
+
closed: () => {
|
|
355
|
+
// Allow closing the underlying WebSocket if the stream was closed before the
|
|
356
|
+
// RSocket connect completed. This should effectively abort the request.
|
|
357
|
+
socket.close();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
socket.addEventListener('message', (event) => {
|
|
362
|
+
resetTimeout();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
return socket;
|
|
366
|
+
}
|
|
367
|
+
}),
|
|
368
|
+
setup: {
|
|
369
|
+
keepAlive: KEEP_ALIVE_MS,
|
|
370
|
+
lifetime: KEEP_ALIVE_LIFETIME_MS,
|
|
371
|
+
dataMimeType: mimeType,
|
|
372
|
+
metadataMimeType: mimeType,
|
|
373
|
+
payload: {
|
|
374
|
+
data: null,
|
|
375
|
+
metadata: toBuffer({
|
|
376
|
+
token: request.headers.Authorization,
|
|
377
|
+
user_agent: userAgent
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
let rsocket: RSocket;
|
|
384
|
+
try {
|
|
385
|
+
rsocket = await connector.connect();
|
|
386
|
+
// The connection is established, we no longer need to monitor the initial timeout
|
|
387
|
+
disposeSocketConnectionTimeout();
|
|
388
|
+
} catch (ex) {
|
|
389
|
+
this.logger.error(`Failed to connect WebSocket`, ex);
|
|
390
|
+
clearTimeout(keepAliveTimeout);
|
|
391
|
+
if (!stream.closed) {
|
|
392
|
+
await stream.close();
|
|
393
|
+
}
|
|
394
|
+
throw ex;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
resetTimeout();
|
|
398
|
+
|
|
399
|
+
let socketIsClosed = false;
|
|
400
|
+
const closeSocket = () => {
|
|
401
|
+
clearTimeout(keepAliveTimeout);
|
|
402
|
+
if (socketIsClosed) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
socketIsClosed = true;
|
|
406
|
+
rsocket.close();
|
|
407
|
+
};
|
|
408
|
+
// Helps to prevent double close scenarios
|
|
409
|
+
rsocket.onClose(() => (socketIsClosed = true));
|
|
410
|
+
// We initially request this amount and expect these to arrive eventually
|
|
411
|
+
let pendingEventsCount = syncQueueRequestSize;
|
|
412
|
+
|
|
413
|
+
const disposeClosedListener = stream.registerListener({
|
|
414
|
+
closed: () => {
|
|
415
|
+
closeSocket();
|
|
416
|
+
disposeClosedListener();
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const socket = await new Promise<Requestable>((resolve, reject) => {
|
|
421
|
+
let connectionEstablished = false;
|
|
422
|
+
|
|
423
|
+
const res = rsocket.requestStream(
|
|
424
|
+
{
|
|
425
|
+
data: toBuffer(options.data),
|
|
426
|
+
metadata: toBuffer({
|
|
427
|
+
path
|
|
428
|
+
})
|
|
429
|
+
},
|
|
430
|
+
syncQueueRequestSize, // The initial N amount
|
|
431
|
+
{
|
|
432
|
+
onError: (e) => {
|
|
433
|
+
if (e.message.includes('PSYNC_')) {
|
|
434
|
+
if (e.message.includes('PSYNC_S21')) {
|
|
435
|
+
this.invalidateCredentials();
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
// Possible that connection is with an older service, always invalidate to be safe
|
|
439
|
+
if (e.message !== 'Closed. ') {
|
|
440
|
+
this.invalidateCredentials();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Don't log closed as an error
|
|
445
|
+
if (e.message !== 'Closed. ') {
|
|
446
|
+
this.logger.error(e);
|
|
447
|
+
}
|
|
448
|
+
// RSocket will close the RSocket stream automatically
|
|
449
|
+
// Close the downstream stream as well - this will close the RSocket connection and WebSocket
|
|
450
|
+
stream.close();
|
|
451
|
+
// Handles cases where the connection failed e.g. auth error or connection error
|
|
452
|
+
if (!connectionEstablished) {
|
|
453
|
+
reject(e);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
onNext: (payload) => {
|
|
457
|
+
// The connection is active
|
|
458
|
+
if (!connectionEstablished) {
|
|
459
|
+
connectionEstablished = true;
|
|
460
|
+
resolve(res);
|
|
461
|
+
}
|
|
462
|
+
const { data } = payload;
|
|
463
|
+
// Less events are now pending
|
|
464
|
+
pendingEventsCount--;
|
|
465
|
+
if (!data) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
stream.enqueueData(data);
|
|
470
|
+
},
|
|
471
|
+
onComplete: () => {
|
|
472
|
+
stream.close();
|
|
473
|
+
},
|
|
474
|
+
onExtension: () => {}
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const l = stream.registerListener({
|
|
480
|
+
lowWater: async () => {
|
|
481
|
+
// Request to fill up the queue
|
|
482
|
+
const required = syncQueueRequestSize - pendingEventsCount;
|
|
483
|
+
if (required > 0) {
|
|
484
|
+
socket.request(syncQueueRequestSize - pendingEventsCount);
|
|
485
|
+
pendingEventsCount = syncQueueRequestSize;
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
closed: () => {
|
|
489
|
+
l();
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
return stream;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Connects to the sync/stream http endpoint, mapping and emitting each received string line.
|
|
498
|
+
*/
|
|
499
|
+
async postStreamRaw<T>(options: SyncStreamOptions, mapLine: (line: string) => T): Promise<DataStream<T>> {
|
|
500
|
+
const { data, path, headers, abortSignal } = options;
|
|
501
|
+
|
|
502
|
+
const request = await this.buildRequest(path);
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* This abort controller will abort pending fetch requests.
|
|
506
|
+
* If the request has resolved, it will be used to close the readable stream.
|
|
507
|
+
* Which will cancel the network request.
|
|
508
|
+
*
|
|
509
|
+
* This nested controller is required since:
|
|
510
|
+
* Aborting the active fetch request while it is being consumed seems to throw
|
|
511
|
+
* an unhandled exception on the window level.
|
|
512
|
+
*/
|
|
513
|
+
if (abortSignal?.aborted) {
|
|
514
|
+
throw new AbortOperation('Abort request received before making postStreamRaw request');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const controller = new AbortController();
|
|
518
|
+
let requestResolved = false;
|
|
519
|
+
abortSignal?.addEventListener('abort', () => {
|
|
520
|
+
if (!requestResolved) {
|
|
521
|
+
// Only abort via the abort controller if the request has not resolved yet
|
|
522
|
+
controller.abort(
|
|
523
|
+
abortSignal.reason ??
|
|
524
|
+
new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.')
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const res = await this.fetch(request.url, {
|
|
530
|
+
method: 'POST',
|
|
531
|
+
headers: { ...headers, ...request.headers },
|
|
532
|
+
body: JSON.stringify(data),
|
|
533
|
+
signal: controller.signal,
|
|
534
|
+
cache: 'no-store',
|
|
535
|
+
...(this.options.fetchOptions ?? {}),
|
|
536
|
+
...options.fetchOptions
|
|
537
|
+
}).catch((ex) => {
|
|
538
|
+
if (ex.name == 'AbortError') {
|
|
539
|
+
throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
|
|
540
|
+
}
|
|
541
|
+
throw ex;
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
if (!res) {
|
|
545
|
+
throw new Error('Fetch request was aborted');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
requestResolved = true;
|
|
549
|
+
|
|
550
|
+
if (!res.ok || !res.body) {
|
|
551
|
+
const text = await res.text();
|
|
552
|
+
this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
|
|
553
|
+
const error: any = new Error(`HTTP ${res.statusText}: ${text}`);
|
|
554
|
+
error.status = res.status;
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Create a new stream splitting the response at line endings while also handling cancellations
|
|
559
|
+
// by closing the reader.
|
|
560
|
+
const reader = res.body.getReader();
|
|
561
|
+
let readerReleased = false;
|
|
562
|
+
// This will close the network request and read stream
|
|
563
|
+
const closeReader = async () => {
|
|
564
|
+
try {
|
|
565
|
+
readerReleased = true;
|
|
566
|
+
await reader.cancel();
|
|
567
|
+
} catch (ex) {
|
|
568
|
+
// an error will throw if the reader hasn't been used yet
|
|
569
|
+
}
|
|
570
|
+
reader.releaseLock();
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
const stream = new DataStream<T, string>({
|
|
575
|
+
logger: this.logger,
|
|
576
|
+
mapLine: mapLine
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
abortSignal?.addEventListener('abort', () => {
|
|
580
|
+
closeReader();
|
|
581
|
+
stream.close();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const decoder = this.createTextDecoder();
|
|
585
|
+
let buffer = '';
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
const l = stream.registerListener({
|
|
590
|
+
lowWater: async () => {
|
|
591
|
+
if (stream.closed || abortSignal?.aborted || readerReleased) {
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
let didCompleteLine = false;
|
|
596
|
+
while (!didCompleteLine) {
|
|
597
|
+
const { done, value } = await reader.read();
|
|
598
|
+
if (done) {
|
|
599
|
+
const remaining = buffer.trim();
|
|
600
|
+
if (remaining.length != 0) {
|
|
601
|
+
stream.enqueueData(remaining);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
stream.close();
|
|
605
|
+
await closeReader();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const data = decoder.decode(value, { stream: true });
|
|
610
|
+
buffer += data;
|
|
611
|
+
|
|
612
|
+
const lines = buffer.split('\n');
|
|
613
|
+
for (var i = 0; i < lines.length - 1; i++) {
|
|
614
|
+
var l = lines[i].trim();
|
|
615
|
+
if (l.length > 0) {
|
|
616
|
+
stream.enqueueData(l);
|
|
617
|
+
didCompleteLine = true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
buffer = lines[lines.length - 1];
|
|
622
|
+
}
|
|
623
|
+
} catch (ex) {
|
|
624
|
+
stream.close();
|
|
625
|
+
throw ex;
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
closed: () => {
|
|
629
|
+
closeReader();
|
|
630
|
+
l?.();
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
return stream;
|
|
635
|
+
}
|
|
636
|
+
}
|