Home Reference Source Test

test/generic/ObjectStoreTestCodec.spec.js

describe('TestCodec', () => {
    let backend, objectStore;

    const setEqual = function(actual, expected) {
        return expected.equals(actual);
    };

    const valuefy = function(obj) {
        const mapFn = value => value.value;
        if (obj === undefined) {
            return obj;
        }
        if (obj instanceof Array) {
            return obj.map(mapFn);
        }
        if (obj instanceof Set) {
            return new Set(Array.from(obj.values(), mapFn));
        }
        return mapFn(obj);
    };

    const expectvaluefy = function(obj) {
        return expect(valuefy(obj));
    };

    const objfy = function(key, value) {
        return { key: key, value: value };
    };

    beforeEach((done) => {
        backend = new InMemoryBackend('', TestCodec.instance);

        objectStore = new ObjectStore(backend, backend);

        (async function () {
            // Add 10 objects.
            for (let i=0; i<10; ++i) {
                await objectStore.put(`key${i}`, objfy(`key${i}`, `value${i}`));
            }
        })().then(done, done.fail);

        jasmine.addCustomEqualityTester(setEqual);
    });

    it('can open a transaction and commit it', (done) => {
        (async function () {
            const tx = objectStore.transaction();
            await tx.remove('key0');
            await tx.put('newKey', objfy('newKey', 'test'));
            expect(await tx.commit()).toBe(true);
            expect(tx.state).toBe(Transaction.STATE.COMMITTED);
            expectvaluefy(await objectStore.get('key0')).toBe(undefined);
            expectvaluefy(await objectStore.get('newKey')).toBe('test');
        })().then(done, done.fail);
    });

    it('can only commit one transaction and ensures read isolation', (done) => {
        (async function () {
            // Create two transactions on the main state.
            const tx1 = objectStore.transaction();
            // Remove a key in one of those.
            await tx1.remove('key0');
            await tx1.put('test', objfy('test', 'success'));
            await tx1.put('key1', objfy('key1', 'someval'));

            const tx2 = objectStore.transaction();
            // Ensure read isolation.
            expectvaluefy(await tx1.get('key0')).toBe(undefined);
            expectvaluefy(await tx2.get('key0')).toBe('value0');
            expectvaluefy(await tx1.get('test')).toBe('success');
            expectvaluefy(await tx2.get('test')).toBe(undefined);
            expectvaluefy(await tx1.get('key1')).toBe('someval');
            expectvaluefy(await tx2.get('key1')).toBe('value1');

            // Commit one transaction.
            expect(await tx1.commit()).toBe(true);
            expect(tx1.state).toBe(Transaction.STATE.COMMITTED);

            // Still ensure read isolation.
            expectvaluefy(await tx1.get('key0')).toBe(undefined);
            expectvaluefy(await tx2.get('key0')).toBe('value0');
            expectvaluefy(await tx1.get('test')).toBe('success');
            expectvaluefy(await tx2.get('test')).toBe(undefined);
            expectvaluefy(await tx1.get('key1')).toBe('someval');
            expectvaluefy(await tx2.get('key1')).toBe('value1');

            // Create a third transaction, which should be based on tx1.
            const tx3 = objectStore.transaction();
            expectvaluefy(await tx3.get('key0')).toBe(undefined);
            expectvaluefy(await tx3.get('test')).toBe('success');
            expectvaluefy(await tx3.get('key1')).toBe('someval');
            expectvaluefy(await backend.get('key0')).toBe('value0'); // not yet written
            expectvaluefy(await backend.get('test')).toBe(undefined); // not yet written
            expectvaluefy(await backend.get('key1')).toBe('value1'); // not yet written

            // More changes
            await tx3.remove('key2');
            await tx3.put('test', objfy('test', 'success2'));
            await tx3.put('key0', objfy('key0', 'someval'));

            // Still ensure read isolation.
            expectvaluefy(await tx1.get('key0')).toBe(undefined);
            expectvaluefy(await tx3.get('key0')).toBe('someval');
            expectvaluefy(await tx1.get('test')).toBe('success');
            expectvaluefy(await tx3.get('test')).toBe('success2');
            expectvaluefy(await tx1.get('key1')).toBe('someval');
            expectvaluefy(await tx3.get('key1')).toBe('someval');
            expectvaluefy(await tx1.get('key2')).toBe('value2');
            expectvaluefy(await tx3.get('key2')).toBe(undefined);

            // Commit third transaction.
            expect(await tx3.commit()).toBe(true);
            expect(tx3.state).toBe(Transaction.STATE.COMMITTED);

            // Create a fourth transaction, which should be based on tx3.
            const tx4 = objectStore.transaction();
            expectvaluefy(await tx4.get('key0')).toBe('someval');
            expectvaluefy(await tx4.get('test')).toBe('success2');
            expectvaluefy(await tx4.get('key1')).toBe('someval');
            expectvaluefy(await tx4.get('key2')).toBe(undefined);
            expectvaluefy(await backend.get('key0')).toBe('value0'); // not yet written
            expectvaluefy(await backend.get('test')).toBe(undefined); // not yet written
            expectvaluefy(await backend.get('key1')).toBe('value1'); // not yet written
            expectvaluefy(await backend.get('key2')).toBe('value2'); // not yet written

            // Abort second transaction and commit empty fourth transaction.
            expect(await tx2.abort()).toBe(true);
            expect(await tx4.commit()).toBe(true);
            expect(tx4.state).toBe(Transaction.STATE.COMMITTED);

            // Now everything should be in the backend.
            expectvaluefy(await backend.get('key0')).toBe('someval');
            expectvaluefy(await objectStore.get('key0')).toBe('someval');
            expectvaluefy(await backend.get('test')).toBe('success2');
            expectvaluefy(await objectStore.get('test')).toBe('success2');
            expectvaluefy(await backend.get('key1')).toBe('someval');
            expectvaluefy(await objectStore.get('key1')).toBe('someval');
            expectvaluefy(await backend.get('key2')).toBe(undefined);
            expectvaluefy(await objectStore.get('key2')).toBe(undefined);

            // Create a fifth transaction, which should be based on the new state.
            const tx5 = objectStore.transaction();
            expectvaluefy(await tx5.get('key0')).toBe('someval');
            expectvaluefy(await tx5.get('test')).toBe('success2');
            expectvaluefy(await tx5.get('key1')).toBe('someval');
            expectvaluefy(await tx5.get('key2')).toBe(undefined);
            await tx5.abort();
        })().then(done, done.fail);
    });

    it('can correctly handle multi-layered transactions', (done) => {
        (async function () {
            // Create two transactions on the main state.
            const tx1 = objectStore.transaction();
            const tx2 = objectStore.transaction();
            // Remove a key in one of those.
            await tx1.remove('key0');
            // Ensure read isolation.
            expect(await tx1.get('key0')).toBe(undefined);
            expectvaluefy(await tx2.get('key0')).toBe('value0');

            // Commit one transaction.
            expect(await tx1.commit()).toBe(true);
            expect(tx1.state).toBe(Transaction.STATE.COMMITTED);

            // Still ensure read isolation.
            expect(await tx1.get('key0')).toBe(undefined);
            expectvaluefy(await tx2.get('key0')).toBe('value0');

            // Create a third transaction, which should be based on tx1.
            const tx3 = objectStore.transaction();
            await tx3.put('test', objfy('test', 'successful'));
            expect(await tx3.get('key0')).toBe(undefined);
            expectvaluefy(await backend.get('key0')).toBe('value0'); // not yet written

            // Should not be able to commit tx2.
            expect(await tx2.commit()).toBe(false);
            expect(tx2.state).toBe(Transaction.STATE.CONFLICTED);

            // tx1 might be flushed by now. No more transactions possible on top of it.
            try {
                tx1.transaction();
                expect(true).toBe(false);
            } catch (e) {
                expect(true).toBe(true);
            }

            // Create another transaction, which should be based on the same backend as tx3.
            const tx4 = objectStore.transaction();
            expect(tx3._parent).toBe(tx4._parent);
            expect(await tx4.get('key0')).toBe(undefined);
            expect(await tx4.get('test')).toBe(undefined);

            // Abort third transaction.
            expect(await tx3.commit()).toBe(true);
            expect(await tx4.commit()).toBe(false);

            // Now tx1 should be in the backend.
            expect(await backend.get('key0')).toBe(undefined);
            expect(await objectStore.get('key0')).toBe(undefined);
            // As well as tx3.
            expectvaluefy(await backend.get('test')).toBe('successful');
            expectvaluefy(await objectStore.get('test')).toBe('successful');

            // Create a fourth transaction, which should be based on the new state.
            const tx5 = objectStore.transaction();
            expect(await tx5.get('key0')).toBe(undefined);
            expectvaluefy(await tx5.get('test')).toBe('successful');
            await tx5.abort();
        })().then(done, done.fail);
    });
});