@powersync/service-module-postgres 0.17.2 → 0.19.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/CHANGELOG.md +49 -0
- package/dist/replication/WalStream.d.ts +9 -2
- package/dist/replication/WalStream.js +29 -10
- package/dist/replication/WalStream.js.map +1 -1
- package/dist/replication/replication-utils.js +1 -1
- package/dist/replication/replication-utils.js.map +1 -1
- package/dist/types/types.d.ts +3 -0
- package/package.json +11 -11
- package/src/replication/WalStream.ts +36 -17
- package/src/replication/replication-utils.ts +1 -1
- package/test/src/checkpoints.test.ts +6 -8
- package/test/src/chunked_snapshots.test.ts +10 -5
- package/test/src/large_batch.test.ts +16 -28
- package/test/src/pg_test.test.ts +5 -5
- package/test/src/resuming_snapshots.test.ts +24 -20
- package/test/src/route_api_adapter.test.ts +5 -3
- package/test/src/schema_changes.test.ts +74 -92
- package/test/src/slow_tests.test.ts +134 -29
- package/test/src/storage_combination.test.ts +2 -2
- package/test/src/util.ts +38 -10
- package/test/src/validation.test.ts +3 -2
- package/test/src/wal_stream.test.ts +33 -42
- package/test/src/wal_stream_utils.ts +80 -42
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import { PgManager } from '@module/replication/PgManager.js';
|
|
2
2
|
import { PUBLICATION_NAME, WalStream, WalStreamOptions } from '@module/replication/WalStream.js';
|
|
3
|
+
import { ReplicationAbortedError } from '@powersync/lib-services-framework';
|
|
3
4
|
import {
|
|
4
5
|
BucketStorageFactory,
|
|
5
6
|
createCoreReplicationMetrics,
|
|
6
7
|
initializeCoreReplicationMetrics,
|
|
7
8
|
InternalOpId,
|
|
9
|
+
LEGACY_STORAGE_VERSION,
|
|
8
10
|
OplogEntry,
|
|
11
|
+
settledPromise,
|
|
9
12
|
storage,
|
|
10
|
-
|
|
13
|
+
STORAGE_VERSION_CONFIG,
|
|
14
|
+
SyncRulesBucketStorage,
|
|
15
|
+
unsettledPromise,
|
|
16
|
+
updateSyncRulesFromYaml
|
|
11
17
|
} from '@powersync/service-core';
|
|
12
|
-
import { METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
|
|
18
|
+
import { bucketRequest, METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
|
|
13
19
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
14
20
|
import { clearTestDb, getClientCheckpoint, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
15
|
-
import { CustomTypeRegistry } from '@module/types/registry.js';
|
|
16
21
|
|
|
17
22
|
export class WalStreamTestContext implements AsyncDisposable {
|
|
18
23
|
private _walStream?: WalStream;
|
|
19
24
|
private abortController = new AbortController();
|
|
20
|
-
private
|
|
25
|
+
private syncRulesId?: number;
|
|
26
|
+
private syncRulesContent?: storage.PersistedSyncRulesContent;
|
|
21
27
|
public storage?: SyncRulesBucketStorage;
|
|
22
|
-
private
|
|
23
|
-
private snapshotPromise?: Promise<void>;
|
|
28
|
+
private settledReplicationPromise?: Promise<PromiseSettledResult<void>>;
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
31
|
* Tests operating on the wal stream need to configure the stream and manage asynchronous
|
|
@@ -30,7 +35,7 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
30
35
|
*/
|
|
31
36
|
static async open(
|
|
32
37
|
factory: (options: storage.TestStorageOptions) => Promise<BucketStorageFactory>,
|
|
33
|
-
options?: { doNotClear?: boolean; walStreamOptions?: Partial<WalStreamOptions> }
|
|
38
|
+
options?: { doNotClear?: boolean; storageVersion?: number; walStreamOptions?: Partial<WalStreamOptions> }
|
|
34
39
|
) {
|
|
35
40
|
const f = await factory({ doNotClear: options?.doNotClear });
|
|
36
41
|
const connectionManager = new PgManager(TEST_CONNECTION_OPTIONS, {});
|
|
@@ -39,13 +44,18 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
39
44
|
await clearTestDb(connectionManager.pool);
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
const storageVersion = options?.storageVersion ?? LEGACY_STORAGE_VERSION;
|
|
48
|
+
const versionedBuckets = STORAGE_VERSION_CONFIG[storageVersion]?.versionedBuckets ?? false;
|
|
49
|
+
|
|
50
|
+
return new WalStreamTestContext(f, connectionManager, options?.walStreamOptions, storageVersion, versionedBuckets);
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
constructor(
|
|
46
54
|
public factory: BucketStorageFactory,
|
|
47
55
|
public connectionManager: PgManager,
|
|
48
|
-
private walStreamOptions?: Partial<WalStreamOptions
|
|
56
|
+
private walStreamOptions?: Partial<WalStreamOptions>,
|
|
57
|
+
private storageVersion: number = LEGACY_STORAGE_VERSION,
|
|
58
|
+
private versionedBuckets: boolean = STORAGE_VERSION_CONFIG[storageVersion]?.versionedBuckets ?? false
|
|
49
59
|
) {
|
|
50
60
|
createCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
|
|
51
61
|
initializeCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
|
|
@@ -55,21 +65,10 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
55
65
|
await this.dispose();
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
/**
|
|
59
|
-
* Clear any errors from startStream, to allow for a graceful dispose when streaming errors
|
|
60
|
-
* were expected.
|
|
61
|
-
*/
|
|
62
|
-
async clearStreamError() {
|
|
63
|
-
if (this.streamPromise != null) {
|
|
64
|
-
this.streamPromise = this.streamPromise.catch((e) => {});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
68
|
async dispose() {
|
|
69
69
|
this.abortController.abort();
|
|
70
70
|
try {
|
|
71
|
-
await this.
|
|
72
|
-
await this.streamPromise;
|
|
71
|
+
await this.settledReplicationPromise;
|
|
73
72
|
await this.connectionManager.destroy();
|
|
74
73
|
await this.factory?.[Symbol.asyncDispose]();
|
|
75
74
|
} catch (e) {
|
|
@@ -95,7 +94,11 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
async updateSyncRules(content: string) {
|
|
98
|
-
const syncRules = await this.factory.updateSyncRules(
|
|
97
|
+
const syncRules = await this.factory.updateSyncRules(
|
|
98
|
+
updateSyncRulesFromYaml(content, { validate: true, storageVersion: this.storageVersion })
|
|
99
|
+
);
|
|
100
|
+
this.syncRulesId = syncRules.id;
|
|
101
|
+
this.syncRulesContent = syncRules;
|
|
99
102
|
this.storage = this.factory.getInstance(syncRules);
|
|
100
103
|
return this.storage!;
|
|
101
104
|
}
|
|
@@ -106,6 +109,8 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
106
109
|
throw new Error(`Next sync rules not available`);
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
this.syncRulesId = syncRules.id;
|
|
113
|
+
this.syncRulesContent = syncRules;
|
|
109
114
|
this.storage = this.factory.getInstance(syncRules);
|
|
110
115
|
return this.storage!;
|
|
111
116
|
}
|
|
@@ -116,10 +121,19 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
116
121
|
throw new Error(`Active sync rules not available`);
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
this.syncRulesId = syncRules.id;
|
|
125
|
+
this.syncRulesContent = syncRules;
|
|
119
126
|
this.storage = this.factory.getInstance(syncRules);
|
|
120
127
|
return this.storage!;
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
private getSyncRulesContent(): storage.PersistedSyncRulesContent {
|
|
131
|
+
if (this.syncRulesContent == null) {
|
|
132
|
+
throw new Error('Sync rules not configured - call updateSyncRules() first');
|
|
133
|
+
}
|
|
134
|
+
return this.syncRulesContent;
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
get walStream() {
|
|
124
138
|
if (this.storage == null) {
|
|
125
139
|
throw new Error('updateSyncRules() first');
|
|
@@ -143,43 +157,46 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
143
157
|
*/
|
|
144
158
|
async initializeReplication() {
|
|
145
159
|
await this.replicateSnapshot();
|
|
146
|
-
this.startStreaming();
|
|
147
160
|
// Make sure we're up to date
|
|
148
161
|
await this.getCheckpoint();
|
|
149
162
|
}
|
|
150
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Replicate the initial snapshot, and start streaming.
|
|
166
|
+
*/
|
|
151
167
|
async replicateSnapshot() {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
// Use a settledPromise to avoid unhandled rejections
|
|
169
|
+
this.settledReplicationPromise = settledPromise(this.walStream.replicate());
|
|
170
|
+
try {
|
|
171
|
+
await Promise.race([unsettledPromise(this.settledReplicationPromise), this.walStream.waitForInitialSnapshot()]);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
if (e instanceof ReplicationAbortedError && e.cause != null) {
|
|
174
|
+
// Edge case for tests: replicate() can throw an error, but we'd receive the ReplicationAbortedError from
|
|
175
|
+
// waitForInitialSnapshot() first. In that case, prioritize the cause, e.g. MissingReplicationSlotError.
|
|
176
|
+
// This is not a concern for production use, since we only use waitForInitialSnapshot() in tests.
|
|
177
|
+
throw e.cause;
|
|
178
|
+
}
|
|
179
|
+
throw e;
|
|
163
180
|
}
|
|
164
|
-
this.streamPromise = this.walStream.streamChanges(this.replicationConnection!);
|
|
165
181
|
}
|
|
166
182
|
|
|
167
183
|
async getCheckpoint(options?: { timeout?: number }) {
|
|
168
184
|
let checkpoint = await Promise.race([
|
|
169
185
|
getClientCheckpoint(this.pool, this.factory, { timeout: options?.timeout ?? 15_000 }),
|
|
170
|
-
this.
|
|
186
|
+
unsettledPromise(this.settledReplicationPromise!)
|
|
171
187
|
]);
|
|
172
188
|
if (checkpoint == null) {
|
|
173
|
-
// This indicates an issue with the test setup -
|
|
189
|
+
// This indicates an issue with the test setup - replicationPromise completed instead
|
|
174
190
|
// of getClientCheckpoint()
|
|
175
|
-
throw new Error('Test failure -
|
|
191
|
+
throw new Error('Test failure - replicationPromise completed');
|
|
176
192
|
}
|
|
177
193
|
return checkpoint;
|
|
178
194
|
}
|
|
179
195
|
|
|
180
196
|
async getBucketsDataBatch(buckets: Record<string, InternalOpId>, options?: { timeout?: number }) {
|
|
181
197
|
let checkpoint = await this.getCheckpoint(options);
|
|
182
|
-
const
|
|
198
|
+
const syncRules = this.getSyncRulesContent();
|
|
199
|
+
const map = Object.entries(buckets).map(([bucket, start]) => bucketRequest(syncRules, bucket, start));
|
|
183
200
|
return test_utils.fromAsync(this.storage!.getBucketDataBatch(checkpoint, map));
|
|
184
201
|
}
|
|
185
202
|
|
|
@@ -191,8 +208,9 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
191
208
|
if (typeof start == 'string') {
|
|
192
209
|
start = BigInt(start);
|
|
193
210
|
}
|
|
211
|
+
const syncRules = this.getSyncRulesContent();
|
|
194
212
|
const checkpoint = await this.getCheckpoint(options);
|
|
195
|
-
|
|
213
|
+
let map = [bucketRequest(syncRules, bucket, start)];
|
|
196
214
|
let data: OplogEntry[] = [];
|
|
197
215
|
while (true) {
|
|
198
216
|
const batch = this.storage!.getBucketDataBatch(checkpoint, map);
|
|
@@ -202,11 +220,30 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
202
220
|
if (batches.length == 0 || !batches[0]!.chunkData.has_more) {
|
|
203
221
|
break;
|
|
204
222
|
}
|
|
205
|
-
map
|
|
223
|
+
map = [bucketRequest(syncRules, bucket, BigInt(batches[0]!.chunkData.next_after))];
|
|
206
224
|
}
|
|
207
225
|
return data;
|
|
208
226
|
}
|
|
209
227
|
|
|
228
|
+
async getChecksums(buckets: string[], options?: { timeout?: number }) {
|
|
229
|
+
const checkpoint = await this.getCheckpoint(options);
|
|
230
|
+
const syncRules = this.getSyncRulesContent();
|
|
231
|
+
const versionedBuckets = buckets.map((bucket) => bucketRequest(syncRules, bucket, 0n));
|
|
232
|
+
const checksums = await this.storage!.getChecksums(checkpoint, versionedBuckets);
|
|
233
|
+
|
|
234
|
+
const unversioned = new Map();
|
|
235
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
236
|
+
unversioned.set(buckets[i], checksums.get(versionedBuckets[i].bucket)!);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return unversioned;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async getChecksum(bucket: string, options?: { timeout?: number }) {
|
|
243
|
+
const checksums = await this.getChecksums([bucket], options);
|
|
244
|
+
return checksums.get(bucket);
|
|
245
|
+
}
|
|
246
|
+
|
|
210
247
|
/**
|
|
211
248
|
* This does not wait for a client checkpoint.
|
|
212
249
|
*/
|
|
@@ -215,8 +252,9 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
215
252
|
if (typeof start == 'string') {
|
|
216
253
|
start = BigInt(start);
|
|
217
254
|
}
|
|
255
|
+
const syncRules = this.getSyncRulesContent();
|
|
218
256
|
const { checkpoint } = await this.storage!.getCheckpoint();
|
|
219
|
-
const map =
|
|
257
|
+
const map = [bucketRequest(syncRules, bucket, start)];
|
|
220
258
|
const batch = this.storage!.getBucketDataBatch(checkpoint, map);
|
|
221
259
|
const batches = await test_utils.fromAsync(batch);
|
|
222
260
|
return batches[0]?.chunkData.data ?? [];
|