Skip to content

Commit ab6f540

Browse files
Merge d4adc1e into 5593be6
2 parents 5593be6 + d4adc1e commit ab6f540

File tree

5 files changed

+171
-1
lines changed

5 files changed

+171
-1
lines changed

.changeset/mean-queens-roll.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/firestore": patch
3+
---
4+
5+
Fixes an issue that prevents `waitForPendingWrites()` from resolving in background tabs when multi-tab is used (https://github.com/firebase/firebase-js-sdk/issues/3816).

packages/firestore/src/core/sync_engine.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,7 @@ export async function applyBatchState(
11851185
// NOTE: Both these methods are no-ops for batches that originated from
11861186
// other clients.
11871187
processUserCallback(syncEngineImpl, batchId, error ? error : null);
1188+
triggerPendingWritesCallbacks(syncEngineImpl, batchId);
11881189
removeCachedMutationBatchMetadata(syncEngineImpl.localStore, batchId);
11891190
} else {
11901191
fail(`Unknown batchState: ${batchState}`);

packages/firestore/test/unit/specs/spec_builder.ts

+15
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,14 @@ export class SpecBuilder {
793793
return this;
794794
}
795795

796+
waitForPendingWrites(): this {
797+
this.nextStep();
798+
this.currentStep = {
799+
waitForPendingWrites: true
800+
};
801+
return this;
802+
}
803+
796804
expectUserCallbacks(docs: {
797805
acknowledged?: string[];
798806
rejected?: string[];
@@ -944,6 +952,13 @@ export class SpecBuilder {
944952
return this;
945953
}
946954

955+
expectWaitForPendingWritesEvent(count = 1): this {
956+
this.assertStep('Expectations require previous step');
957+
const currentStep = this.currentStep!;
958+
currentStep.expectedWaitForPendingWritesEvents = count;
959+
return this;
960+
}
961+
947962
private static queryToSpec(query: Query): SpecQuery {
948963
// TODO(dimond): full query support
949964
const spec: SpecQuery = { path: query.path.canonicalString() };

packages/firestore/test/unit/specs/spec_test_runner.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { SnapshotVersion } from '../../../src/core/snapshot_version';
4444
import {
4545
activeLimboDocumentResolutions,
4646
enqueuedLimboDocumentResolutions,
47+
registerPendingWritesCallback,
4748
SyncEngine,
4849
syncEngineListen,
4950
syncEngineUnlisten,
@@ -195,6 +196,7 @@ abstract class TestRunner {
195196
private eventList: QueryEvent[] = [];
196197
private acknowledgedDocs: string[];
197198
private rejectedDocs: string[];
199+
private waitForPendingWritesEvents = 0;
198200
private snapshotsInSyncListeners: Array<Observer<void>>;
199201
private snapshotsInSyncEvents = 0;
200202

@@ -342,6 +344,9 @@ abstract class TestRunner {
342344
this.validateExpectedSnapshotEvents(step.expectedSnapshotEvents!);
343345
await this.validateExpectedState(step.expectedState!);
344346
this.validateSnapshotsInSyncEvents(step.expectedSnapshotsInSyncEvents);
347+
this.validateWaitForPendingWritesEvents(
348+
step.expectedWaitForPendingWritesEvents
349+
);
345350
this.eventList = [];
346351
this.rejectedDocs = [];
347352
this.acknowledgedDocs = [];
@@ -382,6 +387,8 @@ abstract class TestRunner {
382387
return this.doWriteAck(step.writeAck!);
383388
} else if ('failWrite' in step) {
384389
return this.doFailWrite(step.failWrite!);
390+
} else if ('waitForPendingWrites' in step) {
391+
return this.doWaitForPendingWrites();
385392
} else if ('runTimer' in step) {
386393
return this.doRunTimer(step.runTimer!);
387394
} else if ('drainQueue' in step) {
@@ -716,6 +723,14 @@ abstract class TestRunner {
716723
});
717724
}
718725

726+
private async doWaitForPendingWrites(): Promise<void> {
727+
const deferred = new Deferred();
728+
void deferred.promise.then(() => ++this.waitForPendingWritesEvents);
729+
return this.queue.enqueue(() =>
730+
registerPendingWritesCallback(this.syncEngine, deferred)
731+
);
732+
}
733+
719734
private async doRunTimer(timer: string): Promise<void> {
720735
// We assume the timer string is a valid TimerID enum value, but if it's
721736
// not, then there won't be a matching item on the queue and
@@ -912,10 +927,23 @@ abstract class TestRunner {
912927
}
913928
}
914929

930+
private validateWaitForPendingWritesEvents(
931+
expectedCount: number | undefined
932+
): void {
933+
expect(this.waitForPendingWritesEvents).to.eq(
934+
expectedCount || 0,
935+
'for waitForPendingWritesEvents'
936+
);
937+
this.waitForPendingWritesEvents = 0;
938+
}
939+
915940
private validateSnapshotsInSyncEvents(
916941
expectedCount: number | undefined
917942
): void {
918-
expect(this.snapshotsInSyncEvents).to.eq(expectedCount || 0);
943+
expect(this.snapshotsInSyncEvents).to.eq(
944+
expectedCount || 0,
945+
'for snapshotsInSyncEvents'
946+
);
919947
this.snapshotsInSyncEvents = 0;
920948
}
921949

@@ -1334,6 +1362,8 @@ export interface SpecStep {
13341362
writeAck?: SpecWriteAck;
13351363
/** Fail a write */
13361364
failWrite?: SpecWriteFailure;
1365+
/** Add a new `waitForPendingWrites` listener. */
1366+
waitForPendingWrites?: true;
13371367

13381368
/** Fails the listed database actions. */
13391369
failDatabase?: false | PersistenceAction[];
@@ -1387,6 +1417,12 @@ export interface SpecStep {
13871417
* If not provided, the test will fail if the step causes events to be raised.
13881418
*/
13891419
expectedSnapshotsInSyncEvents?: number;
1420+
1421+
/**
1422+
* Optional expected number of waitForPendingWrite callbacks to be called.
1423+
* If not provided, the test will fail if the step causes events to be raised.
1424+
*/
1425+
expectedWaitForPendingWritesEvents?: number;
13901426
}
13911427

13921428
/** [<target-id>, <query-path>] */

packages/firestore/test/unit/specs/write_spec.test.ts

+113
Original file line numberDiff line numberDiff line change
@@ -1490,4 +1490,117 @@ describeSpec('Writes:', [], () => {
14901490
.expectEvents(query1, { added: [docA, docB], fromCache: true });
14911491
}
14921492
);
1493+
1494+
specTest(
1495+
'Wait for pending writes resolves after write acknowledgment',
1496+
[],
1497+
() => {
1498+
return spec()
1499+
.userSets('collection/a', { k: 'a' })
1500+
.userSets('collection/b', { k: 'b' })
1501+
.waitForPendingWrites()
1502+
.writeAcks('collection/a', 1001)
1503+
.failWrite(
1504+
'collection/b',
1505+
new RpcError(Code.FAILED_PRECONDITION, 'Write error')
1506+
)
1507+
.expectWaitForPendingWritesEvent();
1508+
}
1509+
);
1510+
1511+
specTest('Wait for pending writes resolves with no writes', [], () => {
1512+
return spec().waitForPendingWrites().expectWaitForPendingWritesEvent();
1513+
});
1514+
1515+
specTest('Wait for pending writes resolves multiple times', [], () => {
1516+
return spec()
1517+
.userSets('collection/a', { k: 'a' })
1518+
.waitForPendingWrites()
1519+
.waitForPendingWrites()
1520+
.writeAcks('collection/a', 1001)
1521+
.expectWaitForPendingWritesEvent(2);
1522+
});
1523+
1524+
specTest(
1525+
'Wait for pending writes resolves if another write is issued',
1526+
[],
1527+
() => {
1528+
return spec()
1529+
.userSets('collection/a', { k: 'a' })
1530+
.waitForPendingWrites()
1531+
.userSets('collection/b', { k: 'b' })
1532+
.writeAcks('collection/a', 1001)
1533+
.expectWaitForPendingWritesEvent()
1534+
.writeAcks('collection/b', 1002);
1535+
}
1536+
);
1537+
1538+
specTest(
1539+
'Wait for pending writes waits after restart',
1540+
['durable-persistence'],
1541+
() => {
1542+
return spec()
1543+
.userSets('collection/a', { k: 'a' })
1544+
.restart()
1545+
.waitForPendingWrites()
1546+
.writeAcks('collection/a', 1001, { expectUserCallback: false })
1547+
.expectWaitForPendingWritesEvent();
1548+
}
1549+
);
1550+
1551+
specTest(
1552+
'Wait for pending writes resolves for write in secondary tab',
1553+
['multi-client'],
1554+
() => {
1555+
return client(0)
1556+
.expectPrimaryState(true)
1557+
.client(1)
1558+
.userSets('collection/a', { k: 'a' })
1559+
.waitForPendingWrites()
1560+
.client(0)
1561+
.writeAcks('collection/a', 1001, { expectUserCallback: false })
1562+
.client(1)
1563+
.expectUserCallbacks({ acknowledged: ['collection/a'] })
1564+
.expectWaitForPendingWritesEvent();
1565+
}
1566+
);
1567+
1568+
specTest(
1569+
'Wait for pending writes resolves independently for different tabs',
1570+
['multi-client'],
1571+
() => {
1572+
return client(0)
1573+
.userSets('collection/a', { k: 'a' })
1574+
.waitForPendingWrites()
1575+
.client(1)
1576+
.userSets('collection/b', { k: 'b' })
1577+
.waitForPendingWrites()
1578+
.client(2)
1579+
.userSets('collection/c', { k: 'c' })
1580+
.waitForPendingWrites()
1581+
.client(0)
1582+
.writeAcks('collection/a', 1001)
1583+
.expectWaitForPendingWritesEvent(/* count= */ 1)
1584+
.client(1)
1585+
.expectWaitForPendingWritesEvent(/* count= */ 0)
1586+
.client(2)
1587+
.expectWaitForPendingWritesEvent(/* count= */ 0)
1588+
.client(0)
1589+
.writeAcks('collection/b', 1002, { expectUserCallback: false })
1590+
.expectWaitForPendingWritesEvent(/* count= */ 0)
1591+
.client(1)
1592+
.expectUserCallbacks({ acknowledged: ['collection/b'] })
1593+
.expectWaitForPendingWritesEvent(/* count= */ 1)
1594+
.client(2)
1595+
.expectWaitForPendingWritesEvent(/* count= */ 0)
1596+
.client(0)
1597+
.writeAcks('collection/c', 1003, { expectUserCallback: false })
1598+
.expectWaitForPendingWritesEvent(/* count= */ 0)
1599+
.client(1)
1600+
.expectWaitForPendingWritesEvent(/* count= */ 0)
1601+
.client(2)
1602+
.expectUserCallbacks({ acknowledged: ['collection/c'] })
1603+
.expectWaitForPendingWritesEvent(/* count= */ 1);
1604+
}
1605+
);
14931606
});

0 commit comments

Comments
 (0)