mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
397 lines
13 KiB
TypeScript
397 lines
13 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { expect } from 'chai';
|
|
|
|
import { Timestamp } from '../../../src/api/timestamp';
|
|
import { queryToTarget } from '../../../src/core/query';
|
|
import { SnapshotVersion } from '../../../src/core/snapshot_version';
|
|
import {
|
|
canonifyTarget,
|
|
Target,
|
|
targetEquals,
|
|
TargetImpl
|
|
} from '../../../src/core/target';
|
|
import { TargetId } from '../../../src/core/types';
|
|
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
|
|
import { Persistence } from '../../../src/local/persistence';
|
|
import { TargetData, TargetPurpose } from '../../../src/local/target_data';
|
|
import { addEqualityMatcher } from '../../util/equality_matcher';
|
|
import {
|
|
filter,
|
|
key,
|
|
query,
|
|
resumeTokenForSnapshot,
|
|
version
|
|
} from '../../util/helpers';
|
|
|
|
import * as persistenceHelpers from './persistence_test_helpers';
|
|
import { TestTargetCache } from './test_target_cache';
|
|
|
|
describe('MemoryTargetCache', () => {
|
|
genericTargetCacheTests(persistenceHelpers.testMemoryEagerPersistence);
|
|
});
|
|
|
|
describe('IndexedDbTargetCache', () => {
|
|
if (!IndexedDbPersistence.isAvailable()) {
|
|
console.warn('No IndexedDB. Skipping IndexedDbTargetCache tests.');
|
|
return;
|
|
}
|
|
|
|
let persistencePromise: Promise<Persistence>;
|
|
beforeEach(async () => {
|
|
persistencePromise = persistenceHelpers.testIndexedDbPersistence();
|
|
});
|
|
|
|
genericTargetCacheTests(() => persistencePromise);
|
|
|
|
it('persists metadata across restarts', async () => {
|
|
const db1 = await persistencePromise;
|
|
|
|
const targetCache1 = new TestTargetCache(db1, db1.getTargetCache());
|
|
expect(await targetCache1.getHighestSequenceNumber()).to.equal(0);
|
|
|
|
const originalSequenceNumber = 1234;
|
|
const targetId = 5;
|
|
const snapshotVersion = SnapshotVersion.fromTimestamp(new Timestamp(1, 2));
|
|
const lastLimboFreeSnapshotVersion = SnapshotVersion.fromTimestamp(
|
|
new Timestamp(3, 4)
|
|
);
|
|
const target = queryToTarget(query('rooms'));
|
|
const targetData = new TargetData(
|
|
target,
|
|
targetId,
|
|
TargetPurpose.Listen,
|
|
originalSequenceNumber,
|
|
snapshotVersion,
|
|
lastLimboFreeSnapshotVersion
|
|
);
|
|
|
|
await targetCache1.addTargetData(targetData);
|
|
// Snapshot version needs to be set separately
|
|
await targetCache1.setTargetsMetadata(
|
|
originalSequenceNumber,
|
|
snapshotVersion
|
|
);
|
|
await db1.shutdown();
|
|
|
|
const db2 = await persistenceHelpers.testIndexedDbPersistence({
|
|
dontPurgeData: true
|
|
});
|
|
const targetCache2 = new TestTargetCache(db2, db2.getTargetCache());
|
|
expect(await targetCache2.getHighestSequenceNumber()).to.equal(
|
|
originalSequenceNumber
|
|
);
|
|
const actualTargetData = await targetCache2.getTargetData(target);
|
|
expect(targetData).to.deep.equal(actualTargetData);
|
|
|
|
const actualSnapshotVersion =
|
|
await targetCache2.getLastRemoteSnapshotVersion();
|
|
expect(snapshotVersion.isEqual(actualSnapshotVersion)).to.be.true;
|
|
await db2.shutdown();
|
|
await persistenceHelpers.clearTestPersistence();
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Defines the set of tests to run against both target cache implementations.
|
|
*/
|
|
function genericTargetCacheTests(
|
|
persistencePromise: () => Promise<Persistence>
|
|
): void {
|
|
addEqualityMatcher({ equalsFn: targetEquals, forType: TargetImpl });
|
|
let cache: TestTargetCache;
|
|
|
|
const QUERY_ROOMS = queryToTarget(query('rooms'));
|
|
const QUERY_HALLS = queryToTarget(query('halls'));
|
|
const QUERY_GARAGES = queryToTarget(query('garages'));
|
|
|
|
/**
|
|
* Creates a new TargetData object from the given parameters, synthesizing
|
|
* a resume token from the snapshot version.
|
|
*/
|
|
let previousSequenceNumber = 0;
|
|
function testTargetData(
|
|
target: Target,
|
|
targetId: TargetId,
|
|
testVersion?: number
|
|
): TargetData {
|
|
if (testVersion === undefined) {
|
|
testVersion = 0;
|
|
}
|
|
const snapshotVersion = version(testVersion);
|
|
const resumeToken = resumeTokenForSnapshot(snapshotVersion);
|
|
return new TargetData(
|
|
target,
|
|
targetId,
|
|
TargetPurpose.Listen,
|
|
++previousSequenceNumber,
|
|
snapshotVersion,
|
|
/* lastLimboFreeSnapshotVersion= */ SnapshotVersion.min(),
|
|
resumeToken
|
|
);
|
|
}
|
|
|
|
let persistence: Persistence;
|
|
beforeEach(async () => {
|
|
persistence = await persistencePromise();
|
|
cache = new TestTargetCache(persistence, persistence.getTargetCache());
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (persistence.started) {
|
|
await persistence.shutdown();
|
|
await persistenceHelpers.clearTestPersistence();
|
|
}
|
|
});
|
|
|
|
it('returns null for target not in cache', () => {
|
|
return cache.getTargetData(QUERY_ROOMS).then(targetData => {
|
|
expect(targetData).to.equal(null);
|
|
});
|
|
});
|
|
|
|
it('can set and read a target', async () => {
|
|
const targetData = testTargetData(QUERY_ROOMS, 1, 1);
|
|
await cache.addTargetData(targetData);
|
|
const read = await cache.getTargetData(targetData.target);
|
|
expect(read).to.deep.equal(targetData);
|
|
});
|
|
|
|
it('handles canonical ID collisions', async () => {
|
|
// Type information is currently lost in our canonicalID implementations so
|
|
// this currently an easy way to force colliding canonicalIDs
|
|
const q1 = queryToTarget(query('a', filter('foo', '==', 1)));
|
|
const q2 = queryToTarget(query('a', filter('foo', '==', '1')));
|
|
expect(canonifyTarget(q1)).to.equal(canonifyTarget(q2));
|
|
|
|
const data1 = testTargetData(q1, 1, 1);
|
|
await cache.addTargetData(data1);
|
|
|
|
// Using the other query should not return the query cache entry despite
|
|
// equal canonicalIDs.
|
|
expect(await cache.getTargetData(q2)).to.equal(null);
|
|
expect(await cache.getTargetData(q1)).to.deep.equal(data1);
|
|
expect(await cache.getTargetCount()).to.equal(1);
|
|
|
|
const data2 = testTargetData(q2, 2, 1);
|
|
await cache.addTargetData(data2);
|
|
expect(await cache.getTargetCount()).to.equal(2);
|
|
|
|
expect(await cache.getTargetData(q1)).to.deep.equal(data1);
|
|
expect(await cache.getTargetData(q2)).to.deep.equal(data2);
|
|
|
|
await cache.removeTargetData(data1);
|
|
expect(await cache.getTargetData(q1)).to.equal(null);
|
|
expect(await cache.getTargetData(q2)).to.deep.equal(data2);
|
|
expect(await cache.getTargetCount()).to.equal(1);
|
|
|
|
await cache.removeTargetData(data2);
|
|
expect(await cache.getTargetData(q1)).to.equal(null);
|
|
expect(await cache.getTargetData(q2)).to.equal(null);
|
|
expect(await cache.getTargetCount()).to.equal(0);
|
|
});
|
|
|
|
it('can set target to new value', async () => {
|
|
await cache.addTargetData(testTargetData(QUERY_ROOMS, 1, 1));
|
|
const updated = testTargetData(QUERY_ROOMS, 1, 2);
|
|
await cache.updateTargetData(updated);
|
|
const retrieved = await cache.getTargetData(updated.target);
|
|
expect(retrieved).to.deep.equal(updated);
|
|
});
|
|
|
|
it('can remove a target', async () => {
|
|
const targetData = testTargetData(QUERY_ROOMS, 1, 1);
|
|
await cache.addTargetData(targetData);
|
|
await cache.removeTargetData(targetData);
|
|
const read = await cache.getTargetData(QUERY_ROOMS);
|
|
expect(read).to.equal(null);
|
|
});
|
|
|
|
it('can remove matching keys when a target is removed', async () => {
|
|
const rooms = testTargetData(QUERY_ROOMS, 1, 1);
|
|
await cache.addTargetData(rooms);
|
|
|
|
const key1 = key('rooms/foo');
|
|
const key2 = key('rooms/bar');
|
|
|
|
expect(await cache.containsKey(key1)).to.equal(false);
|
|
|
|
await cache.addMatchingKeys([key1], rooms.targetId);
|
|
await cache.addMatchingKeys([key2], rooms.targetId);
|
|
|
|
expect(await cache.containsKey(key1)).to.equal(true);
|
|
expect(await cache.containsKey(key2)).to.equal(true);
|
|
|
|
await cache.removeTargetData(rooms);
|
|
expect(await cache.containsKey(key1)).to.equal(false);
|
|
expect(await cache.containsKey(key2)).to.equal(false);
|
|
});
|
|
|
|
it('adds or removes matching keys', async () => {
|
|
const k = key('foo/bar');
|
|
expect(await cache.containsKey(k)).to.equal(false);
|
|
|
|
await cache.addMatchingKeys([k], 1);
|
|
expect(await cache.containsKey(k)).to.equal(true);
|
|
|
|
await cache.addMatchingKeys([k], 2);
|
|
expect(await cache.containsKey(k)).to.equal(true);
|
|
|
|
await cache.removeMatchingKeys([k], 1);
|
|
expect(await cache.containsKey(k)).to.equal(true);
|
|
|
|
await cache.removeMatchingKeys([k], 2);
|
|
expect(await cache.containsKey(k)).to.equal(false);
|
|
});
|
|
|
|
it('can remove matching keys for a targetId', async () => {
|
|
const key1 = key('foo/bar');
|
|
const key2 = key('foo/baz');
|
|
const key3 = key('foo/blah');
|
|
|
|
await cache.addMatchingKeys([key1, key2], 1);
|
|
await cache.addMatchingKeys([key3], 2);
|
|
expect(await cache.containsKey(key1)).to.equal(true);
|
|
expect(await cache.containsKey(key2)).to.equal(true);
|
|
expect(await cache.containsKey(key3)).to.equal(true);
|
|
|
|
await cache.removeMatchingKeysForTargetId(1);
|
|
expect(await cache.containsKey(key1)).to.equal(false);
|
|
expect(await cache.containsKey(key2)).to.equal(false);
|
|
expect(await cache.containsKey(key3)).to.equal(true);
|
|
|
|
await cache.removeMatchingKeysForTargetId(2);
|
|
expect(await cache.containsKey(key1)).to.equal(false);
|
|
expect(await cache.containsKey(key2)).to.equal(false);
|
|
expect(await cache.containsKey(key3)).to.equal(false);
|
|
});
|
|
|
|
it('can get matching keys for targetId', async () => {
|
|
const key1 = key('foo/bar');
|
|
const key2 = key('foo/baz');
|
|
const key3 = key('foo/blah');
|
|
|
|
await cache.addMatchingKeys([key1, key2], 1);
|
|
await cache.addMatchingKeys([key3], 2);
|
|
|
|
expect(await cache.getMatchingKeysForTargetId(1)).to.deep.equal([
|
|
key1,
|
|
key2
|
|
]);
|
|
expect(await cache.getMatchingKeysForTargetId(2)).to.deep.equal([key3]);
|
|
|
|
await cache.addMatchingKeys([key1], 2);
|
|
expect(await cache.getMatchingKeysForTargetId(1)).to.deep.equal([
|
|
key1,
|
|
key2
|
|
]);
|
|
expect(await cache.getMatchingKeysForTargetId(2)).to.deep.equal([
|
|
key1,
|
|
key3
|
|
]);
|
|
});
|
|
|
|
it('can allocate target ID', async () => {
|
|
expect(await cache.allocateTargetId()).to.deep.equal(2);
|
|
const targetData1 = testTargetData(QUERY_ROOMS, 2);
|
|
|
|
await cache.addTargetData(targetData1);
|
|
const key1 = key('rooms/bar');
|
|
const key2 = key('rooms/foo');
|
|
await cache.addMatchingKeys([key1, key2], 2);
|
|
|
|
expect(await cache.allocateTargetId()).to.deep.equal(4);
|
|
|
|
const targetData2 = testTargetData(QUERY_HALLS, 4);
|
|
await cache.addTargetData(targetData2);
|
|
const key3 = key('halls/foo');
|
|
await cache.addMatchingKeys([key3], 4);
|
|
|
|
expect(await cache.allocateTargetId()).to.deep.equal(6);
|
|
|
|
await cache.removeTargetData(targetData2);
|
|
|
|
// Target IDs never come down.
|
|
expect(await cache.allocateTargetId()).to.deep.equal(8);
|
|
|
|
// A target with an empty result set still counts.
|
|
const targetData3 = testTargetData(QUERY_GARAGES, 42);
|
|
await cache.addTargetData(targetData3);
|
|
expect(await cache.allocateTargetId()).to.deep.equal(44);
|
|
|
|
await cache.removeTargetData(targetData1);
|
|
expect(await cache.allocateTargetId()).to.deep.equal(46);
|
|
|
|
await cache.removeTargetData(targetData3);
|
|
expect(await cache.allocateTargetId()).to.deep.equal(48);
|
|
|
|
// Verify that the highestTargetId persists restarts.
|
|
const otherCache = new TestTargetCache(
|
|
persistence,
|
|
persistence.getTargetCache()
|
|
);
|
|
expect(await otherCache.allocateTargetId()).to.deep.equal(50);
|
|
});
|
|
|
|
it('can get / set targets metadata', async () => {
|
|
expect(await cache.getLastRemoteSnapshotVersion()).to.deep.equal(
|
|
SnapshotVersion.min()
|
|
);
|
|
|
|
// Can set the snapshot version.
|
|
return cache
|
|
.setTargetsMetadata(/* highestListenSequenceNumber= */ 0, version(42))
|
|
.then(async () => {
|
|
expect(await cache.getLastRemoteSnapshotVersion()).to.deep.equal(
|
|
version(42)
|
|
);
|
|
})
|
|
.then(async () => {
|
|
// Verify snapshot version persists restarts.
|
|
const otherCache = new TestTargetCache(
|
|
persistence,
|
|
persistence.getTargetCache()
|
|
);
|
|
expect(await otherCache.getLastRemoteSnapshotVersion()).to.deep.equal(
|
|
version(42)
|
|
);
|
|
});
|
|
});
|
|
|
|
it('sets the highest sequence number', async () => {
|
|
const target1 = new TargetData(QUERY_ROOMS, 1, TargetPurpose.Listen, 10);
|
|
await cache.addTargetData(target1);
|
|
const target2 = new TargetData(QUERY_HALLS, 2, TargetPurpose.Listen, 20);
|
|
await cache.addTargetData(target2);
|
|
expect(await cache.getHighestSequenceNumber()).to.equal(20);
|
|
|
|
// Sequence numbers can never come down
|
|
await cache.removeTargetData(target2);
|
|
expect(await cache.getHighestSequenceNumber()).to.equal(20);
|
|
|
|
const target3 = new TargetData(QUERY_GARAGES, 3, TargetPurpose.Listen, 100);
|
|
await cache.addTargetData(target3);
|
|
expect(await cache.getHighestSequenceNumber()).to.equal(100);
|
|
|
|
await cache.removeTargetData(target1);
|
|
expect(await cache.getHighestSequenceNumber()).to.equal(100);
|
|
await cache.removeTargetData(target3);
|
|
expect(await cache.getHighestSequenceNumber()).to.equal(100);
|
|
});
|
|
}
|