mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
* chore(ci): make all firestore integration tests optional * fix: align expressions and pipelines, address review comments, update yarn.lock, and fix package versions * chore: update yarn.lock after merge * fix: restore firestore package version and devDependencies to 4.15.0
471 lines
14 KiB
TypeScript
471 lines
14 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 { newQueryForPath, Query } from '../../../src/core/query';
|
|
import { DocumentKey } from '../../../src/model/document_key';
|
|
import { JsonObject } from '../../../src/model/object_value';
|
|
import { LimitType } from '../../../src/protos/firestore_bundle_proto';
|
|
import { toVersion } from '../../../src/remote/serializer';
|
|
import {
|
|
doc,
|
|
filter,
|
|
query,
|
|
TestSnapshotVersion,
|
|
version,
|
|
wrapObject
|
|
} from '../../util/helpers';
|
|
import {
|
|
JSON_SERIALIZER,
|
|
TEST_DATABASE_ID
|
|
} from '../local/persistence_test_helpers';
|
|
import { TestBundleBuilder } from '../util/bundle_data';
|
|
|
|
import { describeSpec, specTest } from './describe_spec';
|
|
import { client, spec } from './spec_builder';
|
|
|
|
interface TestBundleDocument {
|
|
key: DocumentKey;
|
|
readTime: TestSnapshotVersion;
|
|
createTime?: TestSnapshotVersion;
|
|
updateTime?: TestSnapshotVersion;
|
|
content?: JsonObject<unknown>;
|
|
}
|
|
|
|
interface TestBundledQuery {
|
|
name: string;
|
|
readTime: TestSnapshotVersion;
|
|
query: Query;
|
|
limitType?: LimitType;
|
|
}
|
|
|
|
export function bundleWithDocumentAndQuery(
|
|
testDoc: TestBundleDocument,
|
|
testQuery?: TestBundledQuery
|
|
): string {
|
|
const builder = new TestBundleBuilder(TEST_DATABASE_ID);
|
|
|
|
if (testQuery) {
|
|
builder.addNamedQuery(
|
|
testQuery.name,
|
|
toVersion(JSON_SERIALIZER, version(testQuery.readTime)),
|
|
testQuery.query
|
|
);
|
|
}
|
|
|
|
builder.addDocumentMetadata(
|
|
testDoc.key,
|
|
toVersion(JSON_SERIALIZER, version(testDoc.readTime)),
|
|
!!testDoc.createTime
|
|
);
|
|
if (testDoc.createTime) {
|
|
builder.addDocument(
|
|
testDoc.key,
|
|
toVersion(JSON_SERIALIZER, version(testDoc.createTime)),
|
|
toVersion(JSON_SERIALIZER, version(testDoc.updateTime!)),
|
|
wrapObject(testDoc.content!).value.mapValue.fields!
|
|
);
|
|
}
|
|
return builder.build(
|
|
'test-bundle',
|
|
toVersion(JSON_SERIALIZER, version(testDoc.readTime))
|
|
);
|
|
}
|
|
|
|
describeSpec('Bundles:', [], () => {
|
|
specTest('Newer docs from bundles should overwrite cache', [], () => {
|
|
const query1 = query('collection');
|
|
const docA = doc('collection/a', 1000, { value: 'a' });
|
|
const docAChanged = doc('collection/a', 2999, { value: 'b' });
|
|
|
|
const bundleString = bundleWithDocumentAndQuery({
|
|
key: docA.key,
|
|
readTime: 3000,
|
|
createTime: 1999,
|
|
updateTime: 2999,
|
|
content: { value: 'b' }
|
|
});
|
|
|
|
return (
|
|
spec()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 1000, docA)
|
|
.expectEvents(query1, { added: [docA] })
|
|
// TODO(b/160876443): This currently raises snapshots with
|
|
// `fromCache=false` if users already listen to some queries and bundles
|
|
// has newer version.
|
|
.loadBundle(bundleString)
|
|
.expectEvents(query1, { modified: [docAChanged] })
|
|
);
|
|
});
|
|
|
|
specTest(
|
|
'Newer deleted docs from bundles should delete cached docs',
|
|
[],
|
|
() => {
|
|
const query1 = query('collection');
|
|
const docA = doc('collection/a', 1000, { value: 'a' });
|
|
|
|
const bundleString = bundleWithDocumentAndQuery({
|
|
key: docA.key,
|
|
readTime: 3000
|
|
});
|
|
|
|
return spec()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 1000, docA)
|
|
.expectEvents(query1, { added: [docA] })
|
|
.loadBundle(bundleString)
|
|
.expectEvents(query1, { removed: [docA] });
|
|
}
|
|
);
|
|
|
|
specTest('Older deleted docs from bundles should do nothing', [], () => {
|
|
const query1 = query('collection');
|
|
const docA = doc('collection/a', 1000, { value: 'a' });
|
|
|
|
const bundleString = bundleWithDocumentAndQuery({
|
|
key: docA.key,
|
|
readTime: 999
|
|
});
|
|
|
|
return (
|
|
spec()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 1000, docA)
|
|
.expectEvents(query1, { added: [docA] })
|
|
// No events are expected here.
|
|
.loadBundle(bundleString)
|
|
);
|
|
});
|
|
|
|
specTest(
|
|
'Newer docs from bundles should raise snapshot only when Watch catches up with acknowledged writes',
|
|
[],
|
|
() => {
|
|
const query1 = query('collection');
|
|
const docA = doc('collection/a', 250, { value: 'a' });
|
|
|
|
const bundleBeforeMutationAck = bundleWithDocumentAndQuery({
|
|
key: docA.key,
|
|
readTime: 500,
|
|
createTime: 250,
|
|
updateTime: 500,
|
|
content: { value: 'b' }
|
|
});
|
|
|
|
const bundleAfterMutationAck = bundleWithDocumentAndQuery({
|
|
key: docA.key,
|
|
readTime: 1001,
|
|
createTime: 250,
|
|
updateTime: 1001,
|
|
content: { value: 'fromBundle' }
|
|
});
|
|
return (
|
|
spec()
|
|
// TODO(b/160878667): Figure out what happens when memory eager GC is on
|
|
// a bundle is loaded.
|
|
.ensureManualLruGC()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 250, docA)
|
|
.expectEvents(query1, {
|
|
added: [doc('collection/a', 250, { value: 'a' })]
|
|
})
|
|
.userPatches('collection/a', { value: 'patched' })
|
|
.expectEvents(query1, {
|
|
modified: [
|
|
doc('collection/a', 250, {
|
|
value: 'patched'
|
|
}).setHasLocalMutations()
|
|
],
|
|
hasPendingWrites: true
|
|
})
|
|
.writeAcks('collection/a', 1000)
|
|
// loading bundleBeforeMutationAck will not raise snapshots, because its
|
|
// snapshot version is older than the acknowledged mutation.
|
|
.loadBundle(bundleBeforeMutationAck)
|
|
// loading bundleAfterMutationAck will raise a snapshot, because it is after
|
|
// the acknowledged mutation.
|
|
.loadBundle(bundleAfterMutationAck)
|
|
.expectEvents(query1, {
|
|
modified: [doc('collection/a', 1001, { value: 'fromBundle' })]
|
|
})
|
|
);
|
|
}
|
|
);
|
|
|
|
specTest(
|
|
'Newer docs from bundles should keep not raise snapshot if there are unacknowledged writes',
|
|
[],
|
|
() => {
|
|
const query1 = query('collection');
|
|
const docA = doc('collection/a', 250, { value: 'a' });
|
|
|
|
const bundleString = bundleWithDocumentAndQuery({
|
|
key: docA.key,
|
|
readTime: 1001,
|
|
createTime: 250,
|
|
updateTime: 1001,
|
|
content: { value: 'fromBundle' }
|
|
});
|
|
|
|
return (
|
|
spec()
|
|
.ensureManualLruGC()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 250, docA)
|
|
.expectEvents(query1, {
|
|
added: [doc('collection/a', 250, { value: 'a' })]
|
|
})
|
|
.userPatches('collection/a', { value: 'patched' })
|
|
.expectEvents(query1, {
|
|
modified: [
|
|
doc('collection/a', 250, {
|
|
value: 'patched'
|
|
}).setHasLocalMutations()
|
|
],
|
|
hasPendingWrites: true
|
|
})
|
|
// Loading the bundle will not raise snapshots, because the
|
|
// mutation has not been acknowledged.
|
|
.loadBundle(bundleString)
|
|
);
|
|
}
|
|
);
|
|
|
|
specTest('Newer docs from bundles might lead to limbo doc', [], () => {
|
|
const query1 = query('collection');
|
|
const docA = doc('collection/a', 1000, { value: 'a' });
|
|
const bundleString1 = bundleWithDocumentAndQuery({
|
|
key: docA.key,
|
|
readTime: 500,
|
|
createTime: 250,
|
|
updateTime: 500,
|
|
content: { value: 'b' }
|
|
});
|
|
const limboQuery = newQueryForPath(docA.key.path);
|
|
|
|
return (
|
|
spec()
|
|
.ensureManualLruGC()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 250)
|
|
// Backend tells is there is no such doc.
|
|
.expectEvents(query1, {})
|
|
// Bundle tells otherwise, leads to limbo.
|
|
.loadBundle(bundleString1)
|
|
.expectLimboDocs(docA.key)
|
|
.expectEvents(query1, {
|
|
added: [doc('collection/a', 500, { value: 'b' })],
|
|
fromCache: true
|
|
})
|
|
// .watchAcksFull(limboQuery, 1002, docA1)
|
|
.watchAcks(limboQuery)
|
|
.watchSends({ affects: [limboQuery] })
|
|
.watchCurrents(limboQuery, 'resume-token-1002')
|
|
.watchSnapshots(1002)
|
|
.expectLimboDocs()
|
|
.expectEvents(query1, {
|
|
removed: [doc('collection/a', 500, { value: 'b' })],
|
|
fromCache: false
|
|
})
|
|
);
|
|
});
|
|
|
|
specTest(
|
|
'Bundles query can be resumed from same query.',
|
|
['no-pipeline-conversion'],
|
|
() => {
|
|
const query1 = query('collection');
|
|
const docA = doc('collection/a', 100, { key: 'a' });
|
|
const bundleString1 = bundleWithDocumentAndQuery(
|
|
{
|
|
key: docA.key,
|
|
readTime: 500,
|
|
createTime: 250,
|
|
updateTime: 500,
|
|
content: { value: 'b' }
|
|
},
|
|
{ name: 'bundled-query', readTime: 400, query: query1 }
|
|
);
|
|
|
|
return spec()
|
|
.loadBundle(bundleString1)
|
|
.userListens(query1, { readTime: 400 })
|
|
.expectEvents(query1, {
|
|
added: [doc('collection/a', 500, { value: 'b' })],
|
|
fromCache: true
|
|
});
|
|
}
|
|
);
|
|
|
|
specTest(
|
|
'Bundles query can be loaded and resumed from different tabs',
|
|
['multi-client', 'no-pipeline-conversion'],
|
|
() => {
|
|
const query1 = query('collection');
|
|
const query2 = query('collection', filter('value', '==', 'c'));
|
|
const docA = doc('collection/a', 100, { value: 'a' });
|
|
const bundleString1 = bundleWithDocumentAndQuery(
|
|
{
|
|
key: docA.key,
|
|
readTime: 500,
|
|
createTime: 250,
|
|
updateTime: 500,
|
|
content: { value: 'b' }
|
|
},
|
|
{ name: 'bundled-query', readTime: 400, query: query1 }
|
|
);
|
|
|
|
const bundleString2 = bundleWithDocumentAndQuery(
|
|
{
|
|
key: docA.key,
|
|
readTime: 600,
|
|
createTime: 250,
|
|
updateTime: 550,
|
|
content: { value: 'c' }
|
|
},
|
|
{ name: 'bundled-query', readTime: 560, query: query2 }
|
|
);
|
|
|
|
return (
|
|
client(0)
|
|
.loadBundle(bundleString1)
|
|
// Read named query from loaded bundle by primary.
|
|
.client(1)
|
|
.userListens(query1, { readTime: 400 })
|
|
.expectEvents(query1, {
|
|
added: [doc('collection/a', 500, { value: 'b' })],
|
|
fromCache: true
|
|
})
|
|
// Loads a newer bundle.
|
|
.loadBundle(bundleString2)
|
|
.expectEvents(query1, {
|
|
modified: [doc('collection/a', 550, { value: 'c' })],
|
|
fromCache: true
|
|
})
|
|
// Read named query from loaded bundle by secondary.
|
|
.client(0)
|
|
.expectListen(query1, { readTime: 400 })
|
|
.expectActiveTargets({ query: query1, readTime: 400 })
|
|
.userListens(query2, { readTime: 560 })
|
|
.expectEvents(query2, {
|
|
added: [doc('collection/a', 550, { value: 'c' })],
|
|
fromCache: true
|
|
})
|
|
);
|
|
}
|
|
);
|
|
|
|
specTest(
|
|
'Load from secondary clients and observe from primary',
|
|
['multi-client'],
|
|
() => {
|
|
const query1 = query('collection');
|
|
const docA = doc('collection/a', 250, { value: 'a' });
|
|
const bundleString1 = bundleWithDocumentAndQuery({
|
|
key: docA.key,
|
|
readTime: 500,
|
|
createTime: 250,
|
|
updateTime: 500,
|
|
content: { value: 'b' }
|
|
});
|
|
|
|
return client(0)
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 250, docA)
|
|
.expectEvents(query1, {
|
|
added: [docA]
|
|
})
|
|
.client(1)
|
|
.loadBundle(bundleString1)
|
|
.client(0)
|
|
.expectEvents(query1, {
|
|
modified: [doc('collection/a', 500, { value: 'b' })]
|
|
});
|
|
}
|
|
);
|
|
|
|
specTest(
|
|
'Load and observe from same secondary client',
|
|
['multi-client'],
|
|
() => {
|
|
const query1 = query('collection');
|
|
const docA = doc('collection/a', 250, { value: 'a' });
|
|
const bundleString = bundleWithDocumentAndQuery({
|
|
key: docA.key,
|
|
readTime: 500,
|
|
createTime: 250,
|
|
updateTime: 500,
|
|
content: { value: 'b' }
|
|
});
|
|
|
|
return client(0)
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 250, docA)
|
|
.expectEvents(query1, {
|
|
added: [docA]
|
|
})
|
|
.client(1)
|
|
.userListens(query1)
|
|
.expectEvents(query1, {
|
|
added: [docA]
|
|
})
|
|
.loadBundle(bundleString)
|
|
.expectEvents(query1, {
|
|
modified: [doc('collection/a', 500, { value: 'b' })]
|
|
});
|
|
}
|
|
);
|
|
|
|
specTest(
|
|
'Load from primary client and observe from secondary',
|
|
['multi-client'],
|
|
() => {
|
|
const query1 = query('collection');
|
|
const docA = doc('collection/a', 250, { value: 'a' });
|
|
const bundleString1 = bundleWithDocumentAndQuery({
|
|
key: docA.key,
|
|
readTime: 500,
|
|
createTime: 250,
|
|
updateTime: 500,
|
|
content: { value: 'b' }
|
|
});
|
|
|
|
return client(0)
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 250, docA)
|
|
.expectEvents(query1, {
|
|
added: [docA]
|
|
})
|
|
.client(1)
|
|
.userListens(query1)
|
|
.expectEvents(query1, {
|
|
added: [docA]
|
|
})
|
|
.client(0)
|
|
.loadBundle(bundleString1)
|
|
.expectEvents(query1, {
|
|
modified: [doc('collection/a', 500, { value: 'b' })]
|
|
})
|
|
.client(1)
|
|
.expectEvents(query1, {
|
|
modified: [doc('collection/a', 500, { value: 'b' })]
|
|
});
|
|
}
|
|
);
|
|
});
|