Skip to content

Commit 0465d2f

Browse files
Add onCommitted listeners for transactions (#2265)
1 parent 54e1a2f commit 0465d2f

File tree

4 files changed

+159
-19
lines changed

4 files changed

+159
-19
lines changed

packages/firestore/src/local/indexeddb_persistence.ts

+16-15
Original file line numberDiff line numberDiff line change
@@ -749,12 +749,17 @@ export class IndexedDbPersistence implements Persistence {
749749
? 'readwrite-idempotent'
750750
: 'readwrite';
751751

752+
let persistenceTransaction: PersistenceTransaction;
753+
752754
// Do all transactions as readwrite against all object stores, since we
753755
// are the only reader/writer.
754-
return this.simpleDb.runTransaction(
755-
simpleDbMode,
756-
ALL_STORES,
757-
simpleDbTxn => {
756+
return this.simpleDb
757+
.runTransaction(simpleDbMode, ALL_STORES, simpleDbTxn => {
758+
persistenceTransaction = new IndexedDbTransaction(
759+
simpleDbTxn,
760+
this.listenSequence.next()
761+
);
762+
758763
if (mode === 'readwrite-primary') {
759764
// While we merely verify that we have (or can acquire) the lease
760765
// immediately, we wait to extend the primary lease until after
@@ -776,12 +781,7 @@ export class IndexedDbPersistence implements Persistence {
776781
PRIMARY_LEASE_LOST_ERROR_MSG
777782
);
778783
}
779-
return transactionOperation(
780-
new IndexedDbTransaction(
781-
simpleDbTxn,
782-
this.listenSequence.next()
783-
)
784-
);
784+
return transactionOperation(persistenceTransaction);
785785
})
786786
.next(result => {
787787
return this.acquireOrExtendPrimaryLease(simpleDbTxn).next(
@@ -790,13 +790,14 @@ export class IndexedDbPersistence implements Persistence {
790790
});
791791
} else {
792792
return this.verifyAllowTabSynchronization(simpleDbTxn).next(() =>
793-
transactionOperation(
794-
new IndexedDbTransaction(simpleDbTxn, this.listenSequence.next())
795-
)
793+
transactionOperation(persistenceTransaction)
796794
);
797795
}
798-
}
799-
);
796+
})
797+
.then(result => {
798+
persistenceTransaction.raiseOnCommittedEvent();
799+
return result;
800+
});
800801
}
801802

802803
/**

packages/firestore/src/local/memory_persistence.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,11 @@ export class MemoryPersistence implements Persistence {
184184
.onTransactionCommitted(txn)
185185
.next(() => result);
186186
})
187-
.toPromise();
187+
.toPromise()
188+
.then(result => {
189+
txn.raiseOnCommittedEvent();
190+
return result;
191+
});
188192
}
189193

190194
mutationQueuesContainKey(
@@ -203,8 +207,10 @@ export class MemoryPersistence implements Persistence {
203207
* Memory persistence is not actually transactional, but future implementations
204208
* may have transaction-scoped state.
205209
*/
206-
export class MemoryTransaction implements PersistenceTransaction {
207-
constructor(readonly currentSequenceNumber: ListenSequenceNumber) {}
210+
export class MemoryTransaction extends PersistenceTransaction {
211+
constructor(readonly currentSequenceNumber: ListenSequenceNumber) {
212+
super();
213+
}
208214
}
209215

210216
export class MemoryEagerDelegate implements ReferenceDelegate {

packages/firestore/src/local/persistence.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,25 @@ import { RemoteDocumentCache } from './remote_document_cache';
2929
import { ClientId } from './shared_client_state';
3030

3131
/**
32-
* Opaque interface representing a persistence transaction.
32+
* A base class representing a persistence transaction, encapsulating both the
33+
* transaction's sequence numbers as well as a list of onCommitted listeners.
3334
*
3435
* When you call Persistence.runTransaction(), it will create a transaction and
3536
* pass it to your callback. You then pass it to any method that operates
3637
* on persistence.
3738
*/
3839
export abstract class PersistenceTransaction {
40+
private readonly onCommittedListeners: Array<() => void> = [];
41+
3942
abstract readonly currentSequenceNumber: ListenSequenceNumber;
43+
44+
addOnCommittedListener(listener: () => void): void {
45+
this.onCommittedListeners.push(listener);
46+
}
47+
48+
raiseOnCommittedEvent(): void {
49+
this.onCommittedListeners.forEach(listener => listener());
50+
}
4051
}
4152

4253
/** The different modes supported by `IndexedDbPersistence.runTransaction()`. */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import * as persistenceHelpers from './persistence_test_helpers';
19+
import { expect } from 'chai';
20+
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
21+
import { Persistence } from '../../../src/local/persistence';
22+
import { PersistencePromise } from '../../../src/local/persistence_promise';
23+
import { DbTarget, DbTargetKey } from '../../../src/local/indexeddb_schema';
24+
import { TargetId } from '../../../src/core/types';
25+
26+
let persistence: Persistence;
27+
28+
describe('MemoryTransaction', () => {
29+
beforeEach(() => {
30+
return persistenceHelpers.testMemoryEagerPersistence().then(p => {
31+
persistence = p;
32+
});
33+
});
34+
35+
genericTransactionTests();
36+
});
37+
38+
describe('IndexedDbTransaction', () => {
39+
if (!IndexedDbPersistence.isAvailable()) {
40+
console.warn('No IndexedDB. Skipping IndexedDbTransaction tests.');
41+
return;
42+
}
43+
44+
beforeEach(() => {
45+
return persistenceHelpers.testIndexedDbPersistence().then(p => {
46+
persistence = p;
47+
});
48+
});
49+
50+
afterEach(() => persistence.shutdown());
51+
52+
genericTransactionTests();
53+
54+
it('only invokes onCommittedListener once with retries', async () => {
55+
let runCount = 0;
56+
let commitCount = 0;
57+
await persistence.runTransaction(
58+
'onCommitted',
59+
'readwrite-idempotent',
60+
txn => {
61+
const targetsStore = IndexedDbPersistence.getStore<
62+
DbTargetKey,
63+
{ targetId: TargetId }
64+
>(txn, DbTarget.store);
65+
66+
txn.addOnCommittedListener(() => {
67+
++commitCount;
68+
});
69+
70+
expect(commitCount).to.equal(0);
71+
72+
++runCount;
73+
if (runCount === 1) {
74+
// Trigger a unique key violation
75+
return targetsStore
76+
.add({ targetId: 1 })
77+
.next(() => targetsStore.add({ targetId: 1 }));
78+
} else {
79+
return PersistencePromise.resolve(0);
80+
}
81+
}
82+
);
83+
84+
expect(runCount).to.be.equal(2);
85+
expect(commitCount).to.be.equal(1);
86+
});
87+
});
88+
89+
function genericTransactionTests(): void {
90+
it('invokes onCommittedListener when transaction succeeds', async () => {
91+
let onCommitted = false;
92+
await persistence.runTransaction(
93+
'onCommitted',
94+
'readonly-idempotent',
95+
txn => {
96+
txn.addOnCommittedListener(() => {
97+
onCommitted = true;
98+
});
99+
100+
expect(onCommitted).to.be.false;
101+
return PersistencePromise.resolve();
102+
}
103+
);
104+
105+
expect(onCommitted).to.be.true;
106+
});
107+
108+
it('does not invoke onCommittedListener when transaction fails', async () => {
109+
let onCommitted = false;
110+
await persistence
111+
.runTransaction('onCommitted', 'readonly-idempotent', txn => {
112+
txn.addOnCommittedListener(() => {
113+
onCommitted = true;
114+
});
115+
116+
return PersistencePromise.reject(new Error('Aborted'));
117+
})
118+
.catch(() => {});
119+
120+
expect(onCommitted).to.be.false;
121+
});
122+
}

0 commit comments

Comments
 (0)