@powersync/service-module-mssql 0.10.2 → 0.10.4

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.
@@ -5,61 +5,102 @@ import { MSSQLBaseType } from '../types/mssql-data-types.js';
5
5
  import { escapeIdentifier } from '../utils/mssql.js';
6
6
 
7
7
  export interface MSSQLSnapshotQuery {
8
- initialize(): Promise<void>;
8
+ /**
9
+ * Returns the column metadata for the rows that would be returned for this query.
10
+ */
11
+ getColumnMetadata(): Promise<sql.IColumnMetadata>;
9
12
 
10
13
  /**
11
- * Returns an async iterable iterator that yields the column metadata for the query followed by rows of data.
14
+ * Returns an async iterator for the next batch of rows, yielding at most the batch size.
15
+ * A batch smaller than the batch size indicates that there are no rows left.
12
16
  */
13
- next(): AsyncIterableIterator<sql.IColumnMetadata | sql.IRecordSet<any>>;
17
+ nextBatch(): AsyncIterableIterator<Record<string, any>>;
18
+
19
+ /**
20
+ * Cancels the query if it is still in progress.
21
+ */
22
+ close(): Promise<void>;
14
23
  }
15
24
 
16
25
  /**
17
- * Snapshot query using a plain SELECT * FROM table
18
- *
19
- * This supports all tables but does not efficiently resume the snapshot
20
- * if the process is restarted.
26
+ * Helper class that encapsulates the streaming logic of the queries used for snapshots.
27
+ */
28
+ class StreamingQuery {
29
+ readonly rows: AsyncIterableIterator<any>;
30
+ private readonly completed: Promise<void>;
31
+ private finished = false;
32
+
33
+ constructor(
34
+ private readonly request: sql.Request,
35
+ query: string
36
+ ) {
37
+ const stream = request.toReadableStream();
38
+ // Errors are surfaced by iterating the rows. This listener prevents an unhandled 'error' event
39
+ // when the query fails after iteration has stopped, such as the cancellation error from cancel().
40
+ stream.on('error', () => {});
41
+ // In stream mode, errors are emitted on the stream instead of rejecting this promise.
42
+ this.completed = request.query(query).then(
43
+ () => {
44
+ this.finished = true;
45
+ },
46
+ () => {
47
+ this.finished = true;
48
+ }
49
+ );
50
+ this.rows = stream[Symbol.asyncIterator]();
51
+ }
52
+
53
+ async cancel(): Promise<void> {
54
+ if (!this.finished) {
55
+ this.request.cancel();
56
+ }
57
+ // Resolves once the driver has released the transaction's connection.
58
+ await this.completed;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Snapshot query using a plain SELECT * FROM table.
64
+ * This supports all tables but cannot resume the snapshot if the process is restarted.
21
65
  */
22
66
  export class SimpleSnapshotQuery implements MSSQLSnapshotQuery {
67
+ private query: StreamingQuery | null = null;
68
+
23
69
  public constructor(
24
70
  private readonly transaction: sql.Transaction,
25
- private readonly qualifiedTableName: string
71
+ private readonly qualifiedTableName: string,
72
+ private readonly batchSize: number = 10_000
26
73
  ) {}
27
74
 
28
- public async initialize(): Promise<void> {}
29
-
30
- public async *next(): AsyncIterableIterator<sql.IColumnMetadata | sql.IRecordSet<any>> {
31
- const metadataRequest = this.transaction.request();
32
- metadataRequest.stream = true;
33
- const metadataPromise = new Promise<sql.IColumnMetadata>((resolve, reject) => {
34
- metadataRequest.on('recordset', resolve);
35
- metadataRequest.on('error', reject);
36
- });
37
-
38
- metadataRequest.query(`SELECT TOP(0) * FROM ${this.qualifiedTableName}`);
39
-
40
- const columnMetadata: sql.IColumnMetadata = await metadataPromise;
41
- yield columnMetadata;
42
-
43
- const request = this.transaction.request();
44
- const stream = request.toReadableStream();
45
-
46
- request.query(`SELECT * FROM ${this.qualifiedTableName}`);
75
+ public getColumnMetadata(): Promise<sql.IColumnMetadata> {
76
+ return queryColumnMetadata(this.transaction, this.qualifiedTableName);
77
+ }
47
78
 
48
- // MSSQL only streams one row at a time
49
- for await (const row of stream) {
50
- yield row;
79
+ public async *nextBatch(): AsyncIterableIterator<Record<string, any>> {
80
+ // Opened on the first call, and iterated straight away so that the row iterator receives any query error.
81
+ this.query ??= new StreamingQuery(this.transaction.request(), `SELECT * FROM ${this.qualifiedTableName}`);
82
+ for (let i = 0; i < this.batchSize; i++) {
83
+ // MSSQL only streams one row at a time
84
+ const result = await this.query.rows.next();
85
+ if (result.done) {
86
+ return;
87
+ }
88
+ yield result.value;
51
89
  }
52
90
  }
91
+
92
+ public async close(): Promise<void> {
93
+ await this.query?.cancel();
94
+ }
53
95
  }
54
96
 
55
97
  /**
56
98
  * Performs a table snapshot query, batching by ranges of primary key data.
57
99
  *
58
100
  * This may miss some rows if they are modified during the snapshot query.
59
- * In that case, replication will pick up those rows afterward,
60
- * possibly resulting in an IdSnapshotQuery.
101
+ * In that case, replication will pick up those rows afterward.
61
102
  *
62
- * Currently, this only supports a table with a single primary key column,
103
+ * Currently, this only supports tables with a single primary key column,
63
104
  * of a select few types.
64
105
  */
65
106
  export class BatchedSnapshotQuery implements MSSQLSnapshotQuery {
@@ -93,6 +134,7 @@ export class BatchedSnapshotQuery implements MSSQLSnapshotQuery {
93
134
  }
94
135
 
95
136
  private readonly key: ColumnDescriptor;
137
+ private query: StreamingQuery | null = null;
96
138
  lastKey: string | bigint | null = null;
97
139
 
98
140
  public constructor(
@@ -109,57 +151,52 @@ export class BatchedSnapshotQuery implements MSSQLSnapshotQuery {
109
151
  }
110
152
  }
111
153
 
112
- public async initialize(): Promise<void> {
113
- // No-op
114
- }
115
-
116
- public getLastKeySerialized(): Uint8Array {
117
- return bson.serialize({ [this.key.name]: this.lastKey });
118
- }
119
-
120
- public async *next(): AsyncIterableIterator<sql.IColumnMetadata | sql.IRecordSet<any>> {
121
- const escapedKeyName = escapeIdentifier(this.key.name);
122
- const metadataRequest = this.transaction.request();
123
- metadataRequest.stream = true;
124
- const metadataPromise = new Promise<sql.IColumnMetadata>((resolve, reject) => {
125
- metadataRequest.on('recordset', resolve);
126
- metadataRequest.on('error', reject);
127
- });
128
- metadataRequest.query(`SELECT TOP(0) * FROM ${this.qualifiedTableName}`);
129
-
130
- const columnMetadata: sql.IColumnMetadata = await metadataPromise;
154
+ public async getColumnMetadata(): Promise<sql.IColumnMetadata> {
155
+ const columnMetadata = await queryColumnMetadata(this.transaction, this.qualifiedTableName);
131
156
 
132
157
  const foundPrimaryKey = columnMetadata[this.key.name];
133
158
  if (!foundPrimaryKey) {
134
159
  throw new Error(
135
- `Cannot find primary key column ${this.key.name} in results. Keys: ${Object.keys(columnMetadata.columns).join(', ')}`
160
+ `Cannot find primary key column ${this.key.name} in results. Keys: ${Object.keys(columnMetadata).join(', ')}`
136
161
  );
137
162
  }
163
+ return columnMetadata;
164
+ }
138
165
 
139
- yield columnMetadata;
166
+ public getLastKeySerialized(): Uint8Array {
167
+ return bson.serialize({ [this.key.name]: this.lastKey });
168
+ }
140
169
 
170
+ public async *nextBatch(): AsyncIterableIterator<Record<string, any>> {
171
+ const escapedKeyName = escapeIdentifier(this.key.name);
141
172
  const request = this.transaction.request();
142
- const stream = request.toReadableStream();
143
173
  if (this.lastKey == null) {
144
- request.query(`SELECT TOP(${this.batchSize}) * FROM ${this.qualifiedTableName} ORDER BY ${escapedKeyName}`);
174
+ this.query = new StreamingQuery(
175
+ request,
176
+ `SELECT TOP(${this.batchSize}) * FROM ${this.qualifiedTableName} ORDER BY ${escapedKeyName}`
177
+ );
145
178
  } else {
146
179
  if (this.key.typeId == null) {
147
180
  throw new Error(`typeId required for primary key ${this.key.name}`);
148
181
  }
149
- request
150
- .input('lastKey', this.lastKey)
151
- .query(
152
- `SELECT TOP(${this.batchSize}) * FROM ${this.qualifiedTableName} WHERE ${escapedKeyName} > @lastKey ORDER BY ${escapedKeyName}`
153
- );
182
+ request.input('lastKey', this.lastKey);
183
+ this.query = new StreamingQuery(
184
+ request,
185
+ `SELECT TOP(${this.batchSize}) * FROM ${this.qualifiedTableName} WHERE ${escapedKeyName} > @lastKey ORDER BY ${escapedKeyName}`
186
+ );
154
187
  }
155
188
 
156
189
  // MSSQL only streams one row at a time
157
- for await (const row of stream) {
190
+ for await (const row of this.query.rows) {
158
191
  this.lastKey = row[this.key.name];
159
192
  yield row;
160
193
  }
161
194
  }
162
195
 
196
+ public async close(): Promise<void> {
197
+ await this.query?.cancel();
198
+ }
199
+
163
200
  private deserializeKey(key: Uint8Array) {
164
201
  const decoded = bson.deserialize(key, { useBigInt64: true });
165
202
  const keys = Object.keys(decoded);
@@ -173,3 +210,11 @@ export class BatchedSnapshotQuery implements MSSQLSnapshotQuery {
173
210
  return decoded[this.key.name];
174
211
  }
175
212
  }
213
+
214
+ async function queryColumnMetadata(
215
+ transaction: sql.Transaction,
216
+ qualifiedTableName: string
217
+ ): Promise<sql.IColumnMetadata> {
218
+ const { recordset } = await transaction.request().query(`SELECT TOP(0) * FROM ${qualifiedTableName}`);
219
+ return recordset.columns;
220
+ }
@@ -251,28 +251,6 @@ export function toQualifiedTableName(schema: string, tableName: string): string
251
251
  return `${escapeIdentifier(schema)}.${escapeIdentifier(tableName)}`;
252
252
  }
253
253
 
254
- export function isIColumnMetadata(obj: any): obj is sql.IColumnMetadata {
255
- if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
256
- return false;
257
- }
258
-
259
- let propertiesMatched = true;
260
- for (const value of Object.values(obj)) {
261
- const property = value as any;
262
- propertiesMatched =
263
- typeof property.index === 'number' &&
264
- typeof property.name === 'string' &&
265
- (typeof property.length === 'number' || typeof property.length === 'undefined') &&
266
- (typeof property.type === 'function' || typeof property.type === 'object') &&
267
- typeof property.nullable === 'boolean' &&
268
- typeof property.caseSensitive === 'boolean' &&
269
- typeof property.identity === 'boolean' &&
270
- typeof property.readOnly === 'boolean';
271
- }
272
-
273
- return propertiesMatched;
274
- }
275
-
276
254
  export function addParameters(request: sql.Request, parameters: MSSQLParameter[]): sql.Request {
277
255
  for (const param of parameters) {
278
256
  if (param.type) {
@@ -1,11 +1,19 @@
1
+ import { MSSQLConnectionManager } from '@module/replication/MSSQLConnectionManager.js';
1
2
  import { getLatestLSN } from '@module/utils/mssql.js';
3
+ import { ReplicationAbortedError } from '@powersync/lib-services-framework';
2
4
  import { storage } from '@powersync/service-core';
3
5
  import { METRICS_HELPER, putOp, removeOp } from '@powersync/service-core-tests';
4
6
  import { ReplicationMetric } from '@powersync/service-types';
5
7
  import sql from 'mssql';
6
- import { describe, expect, test } from 'vitest';
8
+ import { describe, expect, test, vi } from 'vitest';
7
9
  import { CDCStreamTestContext } from './CDCStreamTestContext.js';
8
- import { createTestTable, describeWithStorage, insertTestData, waitForPendingCDCChanges } from './util.js';
10
+ import {
11
+ createTestTable,
12
+ describeWithStorage,
13
+ enableCDCForTable,
14
+ insertTestData,
15
+ waitForPendingCDCChanges
16
+ } from './util.js';
9
17
 
10
18
  const BASIC_SYNC_RULES = `
11
19
  bucket_definitions:
@@ -14,6 +22,23 @@ bucket_definitions:
14
22
  - SELECT id, description FROM "test_data"
15
23
  `;
16
24
 
25
+ const COMPOSITE_KEY_SYNC_CONFIG = `
26
+ config:
27
+ edition: 3
28
+ streams:
29
+ global:
30
+ auto_subscribe: true
31
+ query: SELECT id_user AS id, id_user, id_visit FROM test_composite
32
+ `;
33
+
34
+ async function getRowsReplicated() {
35
+ return (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
36
+ }
37
+
38
+ async function getTransactionsReplicated() {
39
+ return (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
40
+ }
41
+
17
42
  describe('CDCStream tests', () => {
18
43
  describeWithStorage({ timeout: 20_000 }, defineCDCStreamTests);
19
44
  });
@@ -30,12 +55,12 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
30
55
  const beforeLSN = await getLatestLSN(connectionManager);
31
56
  const testData = await insertTestData(connectionManager, 'test_data');
32
57
  await waitForPendingCDCChanges(beforeLSN, connectionManager);
33
- const startRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
58
+ const startRowCount = await getRowsReplicated();
34
59
 
35
60
  await context.replicateSnapshot();
36
61
  await context.startStreaming();
37
62
 
38
- const endRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
63
+ const endRowCount = await getRowsReplicated();
39
64
  const data = await context.getBucketData('global[]');
40
65
  expect(data).toMatchObject([putOp('test_data', testData)]);
41
66
  expect(endRowCount - startRowCount).toEqual(1);
@@ -49,8 +74,8 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
49
74
  await createTestTable(connectionManager, 'test_data');
50
75
  await context.replicateSnapshot();
51
76
 
52
- const startRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
53
- const startTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
77
+ const startRowCount = await getRowsReplicated();
78
+ const startTxCount = await getTransactionsReplicated();
54
79
 
55
80
  await context.startStreaming();
56
81
 
@@ -59,8 +84,8 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
59
84
  const data = await context.getBucketData('global[]');
60
85
 
61
86
  expect(data).toMatchObject([putOp('test_data', testData)]);
62
- const endRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
63
- const endTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
87
+ const endRowCount = await getRowsReplicated();
88
+ const endTxCount = await getTransactionsReplicated();
64
89
  expect(endRowCount - startRowCount).toEqual(1);
65
90
  expect(endTxCount - startTxCount).toEqual(1);
66
91
  });
@@ -156,8 +181,8 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
156
181
 
157
182
  await context.replicateSnapshot();
158
183
 
159
- const startRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
160
- const startTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
184
+ const startRowCount = await getRowsReplicated();
185
+ const startTxCount = await getTransactionsReplicated();
161
186
 
162
187
  await context.startStreaming();
163
188
 
@@ -165,8 +190,8 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
165
190
  const data = await context.getBucketData('global[]');
166
191
 
167
192
  expect(data).toMatchObject([]);
168
- const endRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
169
- const endTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
193
+ const endRowCount = await getRowsReplicated();
194
+ const endTxCount = await getTransactionsReplicated();
170
195
 
171
196
  // There was a transaction, but it is not counted since it is not for a table in the sync config
172
197
  expect(endRowCount - startRowCount).toEqual(0);
@@ -187,8 +212,8 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
187
212
 
188
213
  await context.replicateSnapshot();
189
214
 
190
- const startRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
191
- const startTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
215
+ const startRowCount = await getRowsReplicated();
216
+ const startTxCount = await getTransactionsReplicated();
192
217
 
193
218
  await context.startStreaming();
194
219
 
@@ -196,9 +221,74 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
196
221
  const data = await context.getBucketData('global[]');
197
222
 
198
223
  expect(data).toMatchObject([putOp('test_DATA', testData)]);
199
- const endRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
200
- const endTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
224
+ const endRowCount = await getRowsReplicated();
225
+ const endTxCount = await getTransactionsReplicated();
201
226
  expect(endRowCount - startRowCount).toEqual(1);
202
227
  expect(endTxCount - startTxCount).toBeGreaterThanOrEqual(1);
203
228
  });
229
+
230
+ // 250 rows end with a partial batch, 300 rows end with an empty batch.
231
+ test.each([250, 300])(
232
+ 'Initial snapshot for a SimpleSnapshotQuery table with %i rows, larger than the snapshot batch size.',
233
+ async (rowCount) => {
234
+ await using context = await CDCStreamTestContext.open(factory, { cdcStreamOptions: { snapshotBatchSize: 100 } });
235
+ const { connectionManager } = context;
236
+ await context.updateSyncRules(COMPOSITE_KEY_SYNC_CONFIG);
237
+
238
+ await createCompositeKeyTable(connectionManager);
239
+ await insertCompositeKeyTableRows(connectionManager, rowCount);
240
+ const startRowCount = await getRowsReplicated();
241
+
242
+ await context.replicateSnapshot();
243
+ await context.startStreaming();
244
+
245
+ const endRowCount = await getRowsReplicated();
246
+ expect(endRowCount - startRowCount).toEqual(rowCount);
247
+ expect(await context.getBucketData('global|0[]')).toHaveLength(rowCount);
248
+ }
249
+ );
250
+
251
+ test('Interrupting initial snapshot for a SimpleSnapshotQuery table between batches', async () => {
252
+ // The snapshot query is still in progress when the snapshot is interrupted between batches.
253
+ // It must be cancelled before the snapshot transaction can be rolled back.
254
+ await using context = await CDCStreamTestContext.open(factory, { cdcStreamOptions: { snapshotBatchSize: 100 } });
255
+ const { connectionManager } = context;
256
+ await context.updateSyncRules(COMPOSITE_KEY_SYNC_CONFIG);
257
+ await createCompositeKeyTable(connectionManager);
258
+ await insertCompositeKeyTableRows(connectionManager, 5000);
259
+ const startRowCount = await getRowsReplicated();
260
+
261
+ const snapshotError = context.replicateSnapshot().then(
262
+ () => null,
263
+ (e) => e
264
+ );
265
+ await vi.waitFor(async () => expect((await getRowsReplicated()) - startRowCount).toBeGreaterThanOrEqual(200), {
266
+ timeout: 10_000,
267
+ interval: 1
268
+ });
269
+ await context.dispose();
270
+
271
+ expect(await snapshotError).toBeInstanceOf(ReplicationAbortedError);
272
+ });
273
+ }
274
+
275
+ /**
276
+ * A composite primary key cannot use BatchedSnapshotQuery, so this table is snapshotted with SimpleSnapshotQuery.
277
+ */
278
+ async function createCompositeKeyTable(connectionManager: MSSQLConnectionManager) {
279
+ await connectionManager.query(`
280
+ CREATE TABLE test_composite (
281
+ id_user INT NOT NULL,
282
+ id_visit INT NOT NULL,
283
+ CONSTRAINT PK_test_composite PRIMARY KEY (id_user, id_visit)
284
+ )`);
285
+ await enableCDCForTable({ connectionManager, table: 'test_composite' });
286
+ }
287
+
288
+ async function insertCompositeKeyTableRows(connectionManager: MSSQLConnectionManager, rowCount: number) {
289
+ const beforeLSN = await getLatestLSN(connectionManager);
290
+ await connectionManager.query(`
291
+ WITH n AS (SELECT 1 AS i UNION ALL SELECT i + 1 FROM n WHERE i < ${rowCount})
292
+ INSERT INTO test_composite (id_user, id_visit) SELECT i, i % 10 FROM n OPTION (MAXRECURSION ${rowCount})`);
293
+ await waitForPendingCDCChanges(beforeLSN, connectionManager);
204
294
  }
@@ -11,7 +11,7 @@ import {
11
11
  SyncRulesBucketStorage,
12
12
  updateSyncRulesFromYaml
13
13
  } from '@powersync/service-core';
14
- import { bucketRequest, METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
14
+ import { bucketRequest, getTestStorage, METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
15
15
  import timers from 'timers/promises';
16
16
  import { clearTestDb, getClientCheckpoint, TEST_CONNECTION_OPTIONS } from './util.js';
17
17
 
@@ -64,7 +64,9 @@ export class CDCStreamTestContext implements AsyncDisposable {
64
64
 
65
65
  async dispose() {
66
66
  this.abortController.abort();
67
- await this.snapshotPromise;
67
+ // Let an interrupted snapshot finish before closing its connections.
68
+ // Its error is handled by the caller of replicateSnapshot().
69
+ await this.snapshotPromise?.catch(() => {});
68
70
  await this.streamPromise;
69
71
  await this.connectionManager.end();
70
72
  await this.factory?.[Symbol.asyncDispose]();
@@ -86,7 +88,7 @@ export class CDCStreamTestContext implements AsyncDisposable {
86
88
  updateSyncRulesFromYaml(content, { validate: true, storageVersion: LEGACY_STORAGE_VERSION })
87
89
  );
88
90
  this.syncRulesContent = replicationStream.syncConfigContent[0];
89
- this.storage = this.factory.getInstance(replicationStream);
91
+ this.storage = await getTestStorage(this.factory, replicationStream);
90
92
  return this.storage!;
91
93
  }
92
94
 
@@ -97,7 +99,7 @@ export class CDCStreamTestContext implements AsyncDisposable {
97
99
  }
98
100
 
99
101
  this.syncRulesContent = syncConfig.content;
100
- this.storage = syncConfig.storage;
102
+ this.storage = await getTestStorage(this.factory, syncConfig.replicationStream);
101
103
  return this.storage!;
102
104
  }
103
105
 
@@ -108,7 +110,7 @@ export class CDCStreamTestContext implements AsyncDisposable {
108
110
  }
109
111
 
110
112
  this.syncRulesContent = syncConfig.content;
111
- this.storage = syncConfig.storage;
113
+ this.storage = await getTestStorage(this.factory, syncConfig.replicationStream);
112
114
  return this.storage!;
113
115
  }
114
116
 
@@ -154,7 +156,8 @@ export class CDCStreamTestContext implements AsyncDisposable {
154
156
  }
155
157
 
156
158
  async replicateSnapshot() {
157
- await this.cdcStream.initReplication();
159
+ this.snapshotPromise = this.cdcStream.initReplication();
160
+ await this.snapshotPromise;
158
161
  this.replicationDone = true;
159
162
  }
160
163