test/generic/CombinedTransactionInMemory.spec.js
describe('CombinedTransaction', () => {
let backend1, backend2, objectStore1, objectStore2;
beforeEach((done) => {
backend1 = new InMemoryBackend('');
backend2 = new InMemoryBackend('');
objectStore1 = new ObjectStore(backend1, null);
objectStore2 = new ObjectStore(backend2, null);
(async function () {
// Add 10 objects.
for (let i=0; i<10; ++i) {
if (i < 8) {
await objectStore1.put(`key${i}`, `value${i}`);
}
if (i > 5) {
await objectStore2.put(`key${i}`, `value${i}`);
}
}
})().then(done, done.fail);
});
it('commit/abort on individual transactions triggers combined commit/abort', (done) => {
(async function () {
let tx1 = objectStore1.transaction();
let tx2 = objectStore2.transaction();
await tx1.remove('key6');
await tx2.remove('key6');
new CombinedTransaction(tx1, tx2);
expect(await tx1.commit()).toBe(true);
expect(await tx2.state).toBe(Transaction.STATE.COMMITTED);
expect(await backend1.get('key6')).toBe(undefined);
expect(await backend2.get('key6')).toBe(undefined);
tx1 = objectStore1.transaction();
tx2 = objectStore2.transaction();
await tx1.remove('key7');
await tx2.remove('key7');
new CombinedTransaction(tx1, tx2);
expect(await tx2.abort()).toBe(true);
expect(await tx1.state).toBe(Transaction.STATE.ABORTED);
expect(await backend1.get('key7')).toBe('value7');
expect(await backend2.get('key7')).toBe('value7');
})().then(done, done.fail);
});
it('cannot commit combined transactions from same object store', (done) => {
(async function () {
const tx1 = objectStore1.transaction();
const tx2 = objectStore1.transaction();
await tx1.remove('key0');
try {
await JungleDB.commitCombined(tx1, tx2);
expect(false).toBe(true);
} catch (e) {
expect(true).toBe(true);
}
})().then(done, done.fail);
});
it('scenario 1: instant flush', (done) => {
(async function () {
// Create two transactions in different object stores.
const tx1 = objectStore1.transaction();
const tx2 = objectStore2.transaction();
await tx1.remove('key6');
await tx2.remove('key6');
// Commit both (which should immediately update the backend as well).
expect(await JungleDB.commitCombined(tx1, tx2)).toBe(true);
expect(await backend1.get('key6')).toBe(undefined);
expect(await backend2.get('key6')).toBe(undefined);
})().then(done, done.fail);
});
it('scenario 2: flush after abort', (done) => {
(async function () {
// Create three transactions in two object stores.
const tx1 = objectStore1.transaction();
const tx2 = objectStore2.transaction();
const tx3 = objectStore2.transaction();
await tx1.remove('key6');
await tx2.remove('key6');
// Commit two of them, which should be successful.
expect(await JungleDB.commitCombined(tx1, tx2)).toBe(true);
// Hence the object store will be updated.
expect(await objectStore1.get('key6')).toBe(undefined);
expect(await objectStore2.get('key6')).toBe(undefined);
// But tx3 should not allow to flush to the backend.
expect(await backend1.get('key6')).toBe('value6');
expect(await backend2.get('key6')).toBe('value6');
// Commit tx3 (which should fail) and expect flushed combined commit.
expect(await tx3.commit()).toBe(false);
expect(await backend1.get('key6')).toBe(undefined);
expect(await backend2.get('key6')).toBe(undefined);
})().then(done, done.fail);
});
it('scenario 3: complex flush after abort', (done) => {
(async function () {
// Create two transactions in different object stores.
const tx1 = objectStore1.transaction();
const tx2 = objectStore2.transaction();
const tx3 = objectStore2.transaction();
await tx1.remove('key6');
await tx2.remove('key6');
expect(await tx2.commit()).toBe(true);
// Create a transaction on top of tx2.
const tx4 = objectStore2.transaction();
await tx4.put('test', 'successful');
// Create a second transaction on top of tx2.
const tx5 = objectStore2.transaction();
// Commit tx4 and tx1.
expect(await JungleDB.commitCombined(tx1, tx4)).toBe(true);
// Hence the object store will be updated.
expect(await objectStore1.get('key6')).toBe(undefined);
expect(await objectStore2.get('key6')).toBe(undefined);
expect(await objectStore2.get('test')).toBe('successful');
// But tx3 should not allow to flush to the backend.
expect(await backend1.get('key6')).toBe('value6');
expect(await backend2.get('key6')).toBe('value6');
expect(await backend2.get('test')).toBe(undefined);
// Commit tx3 (which should fail) and expect no flush yet.
expect(await tx3.commit()).toBe(false);
expect(await objectStore1.get('key6')).toBe(undefined);
expect(await objectStore2.get('key6')).toBe(undefined);
expect(await objectStore2.get('test')).toBe('successful');
// But tx5 should not allow to flush to the backend.
expect(await backend1.get('key6')).toBe('value6');
expect(await backend2.get('test')).toBe(undefined);
// Commit tx5 (which should fail) and expect flushed data.
expect(await tx5.commit()).toBe(false);
expect(await objectStore1.get('key6')).toBe(undefined);
expect(await objectStore2.get('key6')).toBe(undefined);
expect(await objectStore2.get('test')).toBe('successful');
expect(await backend1.get('key6')).toBe(undefined);
expect(await backend2.get('key6')).toBe(undefined);
expect(await backend2.get('test')).toBe('successful');
})().then(done, done.fail);
});
it('scenario 4: no combined nested transactions', (done) => {
(async function () {
// Create three transactions in two different object stores and one nested.
const tx1 = objectStore1.transaction();
const tx2 = objectStore2.transaction();
const tx3 = objectStore1.transaction();
await tx1.remove('key6');
await tx2.remove('key6');
const nested = tx1.transaction();
await nested.put('test', 'successful');
// Cannot commit combined transactions including a nested transaction.
try {
await JungleDB.commitCombined(nested, tx2);
expect(false).toBe(true);
} catch (e) {
expect(true).toBe(true);
}
})().then(done, done.fail);
});
it('scenario 5: simple fail', (done) => {
(async function () {
// Create three transactions in two different object stores.
const tx1 = objectStore1.transaction();
const tx2 = objectStore2.transaction();
const tx3 = objectStore1.transaction();
await tx1.remove('key6');
await tx2.remove('key6');
expect(await tx3.commit()).toBe(true);
// Commit and fail (not all tx are committable because of conflict).
expect(await JungleDB.commitCombined(tx1, tx2)).toBe(false);
expect(await objectStore1.get('key6')).toBe('value6');
expect(await objectStore2.get('key6')).toBe('value6');
expect(await backend1.get('key6')).toBe('value6');
expect(await backend2.get('key6')).toBe('value6');
})().then(done, done.fail);
});
it('scenario 6: complex merge of two combined commits', (done) => {
(async function () {
// Create transactions in different object stores.
const tx1 = objectStore1.transaction();
const tx2 = objectStore1.transaction();
const tx3 = objectStore2.transaction();
await tx1.remove('key6');
await tx3.remove('key6');
// Commit two transactions.
expect(await JungleDB.commitCombined(tx1, tx3)).toBe(true);
// Nothing should be flushed, OS3 should be updated.
expect(await objectStore1.get('key6')).toBe(undefined);
expect(await objectStore2.get('key6')).toBe(undefined);
expect(await backend1.get('key6')).toBe('value6');
expect(await backend2.get('key6')).toBe('value6');
const tx4 = objectStore1.transaction();
const tx5 = objectStore2.transaction();
const tx6 = objectStore2.transaction();
await tx4.put('test', 'successful');
await tx5.put('test', 'successful');
// Next, commit remaining transactions (except tx4) in combination.
expect(await JungleDB.commitCombined(tx4, tx5)).toBe(true);
// And everything should be updated, nothing flushed.
expect(await objectStore1.get('key6')).toBe(undefined);
expect(await objectStore1.get('test')).toBe('successful');
expect(await objectStore2.get('key6')).toBe(undefined);
expect(await objectStore2.get('test')).toBe('successful');
expect(await backend1.get('key6')).toBe('value6');
expect(await backend1.get('test')).toBe(undefined);
expect(await backend2.get('key6')).toBe('value6');
expect(await backend2.get('test')).toBe(undefined);
// Abort tx2, now key6 should be removed.
await tx2.abort();
expect(await backend1.get('key6')).toBe(undefined);
expect(await backend1.get('test')).toBe(undefined);
expect(await backend2.get('key6')).toBe(undefined);
expect(await backend2.get('test')).toBe(undefined);
// Abort tx6, now test should be present.
await tx6.abort();
expect(await backend1.get('key6')).toBe(undefined);
expect(await backend1.get('test')).toBe('successful');
expect(await backend2.get('key6')).toBe(undefined);
expect(await backend2.get('test')).toBe('successful');
})().then(done, done.fail);
});
it('can instantly read the correct value', (done) => {
(async function () {
// Create two transactions in different object stores.
const tx1 = objectStore1.transaction();
const tx2 = objectStore2.transaction();
// Add 7 objects.
for (let i=0; i<7; ++i) {
await tx1.put(`key${i}`, `newvalue${i}`);
await tx2.put(`key${i}`, `newvalue${i}`);
}
// Commit both (which should immediately update the backend as well).
expect(await tx1.get('key6')).toBe('newvalue6');
expect(await tx2.get('key6')).toBe('newvalue6');
expect(await JungleDB.commitCombined(tx1, tx2)).toBe(true);
expect(await objectStore2.get('key6')).toBe('newvalue6');
expect(await objectStore1.get('key6')).toBe('newvalue6');
})().then(done, done.fail);
});
it('does not infinitely stack combined transactions', (done) => {
(async function () {
const blockingTx1 = objectStore1.transaction();
const blockingTx2 = objectStore2.transaction();
let tx1 = objectStore1.transaction();
let tx2 = objectStore2.transaction();
await tx1.remove('key6');
await tx2.remove('key6');
expect(await JungleDB.commitCombined(tx1, tx2)).toBe(true);
expect(objectStore1._stateStack.length).toBe(1);
expect(objectStore2._stateStack.length).toBe(1);
tx1 = objectStore1.transaction();
tx2 = objectStore2.transaction();
await tx1.remove('key5');
await tx2.remove('key5');
expect(await JungleDB.commitCombined(tx1, tx2)).toBe(true);
expect(objectStore1._stateStack.length).toBe(2);
expect(objectStore2._stateStack.length).toBe(2);
tx1 = objectStore1.transaction();
tx2 = objectStore2.transaction();
await tx1.remove('key4');
await tx2.remove('key4');
expect(await JungleDB.commitCombined(tx1, tx2)).toBe(true);
expect(objectStore1._stateStack.length).toBe(3);
expect(objectStore2._stateStack.length).toBe(3);
await blockingTx1.abort();
expect(objectStore1._stateStack.length).toBe(3);
expect(objectStore2._stateStack.length).toBe(3);
await blockingTx2.abort();
expect(objectStore1._stateStack.length).toBe(0);
expect(objectStore2._stateStack.length).toBe(0);
})().then(done, done.fail);
});
it('does not infinitely stack 3 combined transactions', (done) => {
(async function () {
const backend3 = new InMemoryBackend('');
const objectStore3 = new ObjectStore(backend3, null);
const blockingTx1 = objectStore1.transaction();
const blockingTx2 = objectStore2.transaction();
const blockingTx3 = objectStore3.transaction();
let tx1 = objectStore1.transaction();
let tx2 = objectStore2.transaction();
let tx3 = objectStore3.transaction();
await tx1.remove('key6');
await tx2.remove('key6');
await tx3.put('key6', 'test');
expect(await JungleDB.commitCombined(tx1, tx2, tx3)).toBe(true);
expect(objectStore1._stateStack.length).toBe(1);
expect(objectStore2._stateStack.length).toBe(1);
expect(objectStore3._stateStack.length).toBe(1);
tx1 = objectStore1.transaction();
tx2 = objectStore2.transaction();
tx3 = objectStore3.transaction();
await tx1.remove('key5');
await tx2.remove('key5');
await tx3.put('key5', 'test');
expect(await JungleDB.commitCombined(tx1, tx2, tx3)).toBe(true);
expect(objectStore1._stateStack.length).toBe(2);
expect(objectStore2._stateStack.length).toBe(2);
expect(objectStore3._stateStack.length).toBe(2);
tx1 = objectStore1.transaction();
tx2 = objectStore2.transaction();
tx3 = objectStore3.transaction();
await tx1.remove('key4');
await tx2.remove('key4');
await tx3.put('key4', 'test');
expect(await JungleDB.commitCombined(tx1, tx2, tx3)).toBe(true);
expect(objectStore1._stateStack.length).toBe(3);
expect(objectStore2._stateStack.length).toBe(3);
expect(objectStore3._stateStack.length).toBe(3);
await blockingTx1.abort();
expect(objectStore1._stateStack.length).toBe(3);
expect(objectStore2._stateStack.length).toBe(3);
expect(objectStore3._stateStack.length).toBe(3);
await blockingTx2.abort();
expect(objectStore1._stateStack.length).toBe(3);
expect(objectStore2._stateStack.length).toBe(3);
expect(objectStore3._stateStack.length).toBe(3);
await blockingTx3.abort();
expect(objectStore1._stateStack.length).toBe(0);
expect(objectStore2._stateStack.length).toBe(0);
expect(objectStore3._stateStack.length).toBe(0);
})().then(done, done.fail);
});
it('can handle fast incoming combined transactions', (done) => {
(async function () {
const backend3 = new InMemoryBackend('');
const objectStore3 = new ObjectStore(backend3, null);
let tx1 = objectStore1.transaction();
let tx2 = objectStore2.transaction();
let tx3 = objectStore3.transaction();
for (let i = 0; i < ObjectStore.MAX_STACK_SIZE * 2; ++i) {
expect(await JungleDB.commitCombined(tx1, tx2, tx3)).toBe(true);
tx1 = objectStore1.transaction();
tx2 = objectStore2.transaction();
tx3 = objectStore3.transaction();
}
expect(objectStore1._stateStack.length).toBe(0);
expect(objectStore2._stateStack.length).toBe(0);
expect(objectStore3._stateStack.length).toBe(0);
})().then(done, done.fail);
});
});