mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
584 lines
22 KiB
TypeScript
584 lines
22 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 { LimitType, queryWithLimit } from '../../../src/core/query';
|
|
import { TargetPurpose } from '../../../src/local/target_data';
|
|
import { deletedDoc, doc, filter, orderBy, query } from '../../util/helpers';
|
|
|
|
import { describeSpec, specTest } from './describe_spec';
|
|
import { client, spec } from './spec_builder';
|
|
|
|
describeSpec('Limits:', [], () => {
|
|
specTest('Documents in limit are replaced by remote event', [], () => {
|
|
const query1 = queryWithLimit(query('collection'), 2, LimitType.First);
|
|
const doc1 = doc('collection/a', 1000, { key: 'a' });
|
|
const doc2 = doc('collection/b', 1002, { key: 'b' });
|
|
const doc3 = doc('collection/c', 1001, { key: 'c' });
|
|
return spec()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 1001, doc1, doc3)
|
|
.expectEvents(query1, {
|
|
added: [doc1, doc3]
|
|
})
|
|
.watchSends({ affects: [query1] }, doc2)
|
|
.watchSends({ removed: [query1] }, doc3)
|
|
.watchSnapshots(1002)
|
|
.expectEvents(query1, {
|
|
added: [doc2],
|
|
removed: [doc3]
|
|
});
|
|
});
|
|
|
|
specTest(
|
|
"Documents outside of limit don't raise hasPendingWrites",
|
|
[],
|
|
() => {
|
|
const query1 = queryWithLimit(query('collection'), 2, LimitType.First);
|
|
const doc1 = doc('collection/a', 1000, { key: 'a' });
|
|
const doc2 = doc('collection/b', 1000, { key: 'b' });
|
|
|
|
return spec()
|
|
.ensureManualLruGC()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 1000, doc1, doc2)
|
|
.expectEvents(query1, {
|
|
added: [doc1, doc2]
|
|
})
|
|
.userUnlistens(query1)
|
|
.userSets('collection/c', { key: 'c' })
|
|
.userListens(query1, { resumeToken: 'resume-token-1000' })
|
|
.expectEvents(query1, {
|
|
added: [doc1, doc2],
|
|
fromCache: true
|
|
});
|
|
}
|
|
);
|
|
|
|
specTest('Deleted Document in limbo in full limit query', [], () => {
|
|
const query1 = queryWithLimit(query('collection'), 2, LimitType.First);
|
|
const doc1 = doc('collection/a', 1000, { key: 'a' });
|
|
const doc2 = doc('collection/b', 1001, { key: 'b' });
|
|
const doc3 = doc('collection/c', 1002, { key: 'c' });
|
|
return (
|
|
spec()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 1002, doc1, doc2)
|
|
.expectEvents(query1, {
|
|
added: [doc1, doc2]
|
|
})
|
|
.watchResets(query1)
|
|
.watchSends({ affects: [query1] }, doc2, doc3)
|
|
.watchCurrents(query1, 'resume-token-' + 2000)
|
|
.watchSnapshots(2000)
|
|
.expectLimboDocs(doc1.key)
|
|
// Limbo document causes query to be "inconsistent"
|
|
.expectEvents(query1, { fromCache: true })
|
|
.ackLimbo(2000, deletedDoc('collection/a', 2000))
|
|
.expectLimboDocs()
|
|
.expectEvents(query1, {
|
|
added: [doc3],
|
|
removed: [doc1]
|
|
})
|
|
);
|
|
});
|
|
|
|
specTest('Documents in limit can handle removed messages', [], () => {
|
|
const query1 = queryWithLimit(query('collection'), 2, LimitType.First);
|
|
const doc1 = doc('collection/a', 1000, { key: 'a' });
|
|
const doc2 = doc('collection/b', 1002, { key: 'b' });
|
|
const doc3 = doc('collection/c', 1001, { key: 'c' });
|
|
return (
|
|
spec()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 1001, doc1, doc3)
|
|
.expectEvents(query1, {
|
|
added: [doc1, doc3]
|
|
})
|
|
.watchSends({ affects: [query1] }, doc2)
|
|
.watchRemovesDoc(doc3.key, query1)
|
|
.watchSnapshots(1002)
|
|
// No limbo docs expected because doc3 was removed
|
|
.expectLimboDocs()
|
|
.expectEvents(query1, {
|
|
added: [doc2],
|
|
removed: [doc3]
|
|
})
|
|
);
|
|
});
|
|
|
|
specTest(
|
|
'Documents in limit are can handle removed messages for only one of many query',
|
|
[],
|
|
() => {
|
|
const query1 = queryWithLimit(query('collection'), 2, LimitType.First);
|
|
const query2 = queryWithLimit(query('collection'), 3, LimitType.First);
|
|
const doc1 = doc('collection/a', 1000, { key: 'a' });
|
|
const doc2 = doc('collection/b', 1002, { key: 'b' });
|
|
const doc3 = doc('collection/c', 1001, { key: 'c' });
|
|
return (
|
|
spec()
|
|
.userListens(query1)
|
|
.userListens(query2)
|
|
.watchAcks(query1)
|
|
.watchAcks(query2)
|
|
.watchSends({ affects: [query1] }, doc1, doc3)
|
|
.watchSends({ affects: [query2] }, doc1, doc3)
|
|
.watchCurrents(query1, 'resume-token-1001')
|
|
.watchCurrents(query2, 'resume-token-1001')
|
|
.watchSnapshots(1001)
|
|
.expectEvents(query1, { added: [doc1, doc3] })
|
|
.expectEvents(query2, { added: [doc1, doc3] })
|
|
.watchSends({ affects: [query1, query2] }, doc2)
|
|
.watchRemovesDoc(doc3.key, query1)
|
|
.watchSnapshots(1002)
|
|
// No limbo docs expected because doc3 was removed from query1
|
|
.expectLimboDocs()
|
|
.expectEvents(query1, {
|
|
added: [doc2],
|
|
removed: [doc3]
|
|
})
|
|
.expectEvents(query2, { added: [doc2] })
|
|
);
|
|
}
|
|
);
|
|
|
|
specTest('Limits are re-filled from cache', [], () => {
|
|
// This test verifies that our Query handling backfills a limit query from
|
|
// cache even if the backend has not told us that an existing
|
|
// RemoteDocument is within the limit.
|
|
const fullQuery = query('collection', filter('matches', '==', true));
|
|
const limitQuery = queryWithLimit(fullQuery, 2, LimitType.First);
|
|
const doc1 = doc('collection/a', 1001, { matches: true });
|
|
const doc2 = doc('collection/b', 1002, { matches: true });
|
|
const doc3 = doc('collection/c', 1000, { matches: true });
|
|
return spec()
|
|
.ensureManualLruGC()
|
|
.userListens(fullQuery)
|
|
.watchAcksFull(fullQuery, 1002, doc1, doc2, doc3)
|
|
.expectEvents(fullQuery, { added: [doc1, doc2, doc3] })
|
|
.userUnlistens(fullQuery)
|
|
.userListens(limitQuery)
|
|
.expectEvents(limitQuery, { added: [doc1, doc2], fromCache: true })
|
|
.userSets('collection/a', { matches: false })
|
|
.expectEvents(limitQuery, {
|
|
added: [doc3],
|
|
removed: [doc1],
|
|
fromCache: true
|
|
});
|
|
});
|
|
|
|
specTest(
|
|
'Initial snapshots for limit queries are re-filled from cache (with removal)',
|
|
[],
|
|
() => {
|
|
// Verify that views for limit queries are re-filled even if the initial
|
|
// snapshot does not contain the requested number of results.
|
|
const fullQuery = query('collection', filter('matches', '==', true));
|
|
const limitQuery = queryWithLimit(fullQuery, 2, LimitType.First);
|
|
const doc1 = doc('collection/a', 1001, { matches: true });
|
|
const doc2 = doc('collection/b', 1002, { matches: true });
|
|
const doc3 = doc('collection/c', 1003, { matches: true });
|
|
return spec()
|
|
.ensureManualLruGC()
|
|
.userListens(fullQuery)
|
|
.watchAcksFull(fullQuery, 1003, doc1, doc2, doc3)
|
|
.expectEvents(fullQuery, { added: [doc1, doc2, doc3] })
|
|
.userUnlistens(fullQuery)
|
|
.userListens(limitQuery)
|
|
.expectEvents(limitQuery, { added: [doc1, doc2], fromCache: true })
|
|
.watchAcksFull(limitQuery, 1004, doc1, doc2)
|
|
.expectEvents(limitQuery, {})
|
|
.userUnlistens(limitQuery)
|
|
.watchRemoves(limitQuery)
|
|
.userSets('collection/a', { matches: false })
|
|
.userListens(limitQuery, { resumeToken: 'resume-token-1004' })
|
|
.expectEvents(limitQuery, { added: [doc2, doc3], fromCache: true });
|
|
}
|
|
);
|
|
|
|
specTest(
|
|
'Initial snapshots for limit queries are re-filled from cache (with latency-compensated edit)',
|
|
[],
|
|
() => {
|
|
// Verify that views for limit queries contain the correct set of documents
|
|
// even if a previously matching document receives a latency-compensate update
|
|
// that makes it sort below an older document.
|
|
const fullQuery = query('collection');
|
|
const limitQuery = queryWithLimit(
|
|
query('collection', orderBy('pos')),
|
|
2,
|
|
LimitType.First
|
|
);
|
|
const doc1 = doc('collection/a', 1001, { pos: 1 });
|
|
const doc2 = doc('collection/b', 1002, { pos: 2 });
|
|
const doc3 = doc('collection/c', 1003, { pos: 3 });
|
|
return spec()
|
|
.ensureManualLruGC()
|
|
.userListens(fullQuery)
|
|
.watchAcksFull(fullQuery, 1003, doc1, doc2, doc3)
|
|
.expectEvents(fullQuery, { added: [doc1, doc2, doc3] })
|
|
.userUnlistens(fullQuery)
|
|
.watchRemoves(fullQuery)
|
|
.userListens(limitQuery)
|
|
.expectEvents(limitQuery, { added: [doc1, doc2], fromCache: true })
|
|
.watchAcksFull(limitQuery, 1004, doc1, doc2)
|
|
.expectEvents(limitQuery, {})
|
|
.userUnlistens(limitQuery)
|
|
.watchRemoves(limitQuery)
|
|
.userSets('collection/a', { pos: 4 })
|
|
.userListens(limitQuery, { resumeToken: 'resume-token-1004' })
|
|
.expectEvents(limitQuery, { added: [doc2, doc3], fromCache: true });
|
|
}
|
|
);
|
|
|
|
specTest(
|
|
'Initial snapshots for limit queries are re-filled from cache (with update from backend)',
|
|
[],
|
|
() => {
|
|
// Verify that views for limit queries contain the correct set of documents
|
|
// even if a previously matching document receives an update from the backend
|
|
// that makes it sort below an older document.
|
|
const fullQuery = query('collection');
|
|
const limitQuery = queryWithLimit(
|
|
query('collection', orderBy('pos')),
|
|
2,
|
|
LimitType.First
|
|
);
|
|
const doc1 = doc('collection/a', 1001, { pos: 1 });
|
|
const doc1Edited = doc('collection/a', 1005, { pos: 4 });
|
|
const doc2 = doc('collection/b', 1002, { pos: 2 });
|
|
const doc3 = doc('collection/c', 1003, { pos: 3 });
|
|
return spec()
|
|
.ensureManualLruGC()
|
|
.userListens(fullQuery)
|
|
.watchAcksFull(fullQuery, 1003, doc1, doc2, doc3)
|
|
.expectEvents(fullQuery, { added: [doc1, doc2, doc3] })
|
|
.userUnlistens(fullQuery)
|
|
.watchRemoves(fullQuery)
|
|
.userListens(limitQuery)
|
|
.expectEvents(limitQuery, { added: [doc1, doc2], fromCache: true })
|
|
.watchAcksFull(limitQuery, 1004, doc1, doc2)
|
|
.expectEvents(limitQuery, {})
|
|
.userUnlistens(limitQuery)
|
|
.watchRemoves(limitQuery)
|
|
.userListens(fullQuery, { resumeToken: 'resume-token-1003' })
|
|
.expectEvents(fullQuery, { added: [doc1, doc2, doc3], fromCache: true })
|
|
.watchAcksFull(fullQuery, 1005, doc1Edited)
|
|
.expectEvents(fullQuery, { modified: [doc1Edited] })
|
|
.userListens(limitQuery, { resumeToken: 'resume-token-1004' })
|
|
.expectEvents(limitQuery, { added: [doc2, doc3], fromCache: true });
|
|
}
|
|
);
|
|
|
|
specTest(
|
|
'Resumed limit queries exclude deleted documents ',
|
|
['durable-persistence'],
|
|
() => {
|
|
// This test verifies that views for limit queries are updated even
|
|
// when documents are deleted while the query is inactive.
|
|
|
|
const limitQuery = queryWithLimit(
|
|
query('collection', orderBy('a')),
|
|
1,
|
|
LimitType.First
|
|
);
|
|
const fullQuery = query('collection', orderBy('a'));
|
|
|
|
const firstDocument = doc('collection/a', 1001, { a: 1 });
|
|
const firstDocumentDeleted = deletedDoc('collection/a', 1003);
|
|
const secondDocument = doc('collection/b', 1000, { a: 2 });
|
|
|
|
return (
|
|
spec()
|
|
.ensureManualLruGC()
|
|
.userListens(limitQuery)
|
|
.watchAcksFull(limitQuery, 1001, firstDocument)
|
|
.expectEvents(limitQuery, { added: [firstDocument] })
|
|
.userUnlistens(limitQuery)
|
|
.watchRemoves(limitQuery)
|
|
.userListens(fullQuery)
|
|
.expectEvents(fullQuery, { added: [firstDocument], fromCache: true })
|
|
.watchAcksFull(fullQuery, 1002, firstDocument, secondDocument)
|
|
.expectEvents(fullQuery, { added: [secondDocument] })
|
|
// Another client modified `firstDocument` and we lost access to it.
|
|
// Watch sends us a remove, but doesn't tell us the new document state.
|
|
// Since we don't know the state of the document, we mark it as limbo.
|
|
.watchRemovesDoc(firstDocument.key, fullQuery)
|
|
.watchSnapshots(1003)
|
|
.expectEvents(fullQuery, { fromCache: true })
|
|
.expectLimboDocs(firstDocument.key)
|
|
.userUnlistens(fullQuery)
|
|
// Since we stop listening to `fullQuery`, we disregard our attempt to
|
|
// resolve the limbo state of `firstDocument`.
|
|
.expectLimboDocs()
|
|
.watchRemoves(fullQuery)
|
|
// We restart the client, which clears the limbo target mapping in the
|
|
// spec test runner. Without restarting, the runner assumes that each
|
|
// limbo document is always assigned the same target ID. SyncEngine,
|
|
// however, uses new target IDs if a document goes in and out of limbo.
|
|
.restart()
|
|
// We listen to the limit query again. Note that we include
|
|
// `firstDocument` in the local result since we did not resolve its
|
|
// limbo state.
|
|
.userListens(limitQuery, { resumeToken: 'resume-token-1001' })
|
|
.expectEvents(limitQuery, { added: [firstDocument], fromCache: true })
|
|
.watchAcks(limitQuery)
|
|
// Watch resumes the query from the provided resume token, but does
|
|
// not guarantee to send us the removal of `firstDocument`. Instead,
|
|
// we receive an existence filter, which indicates that our view is
|
|
// out of sync.
|
|
.watchSends({ affects: [limitQuery] }, secondDocument)
|
|
.watchFilters([limitQuery], [secondDocument.key])
|
|
.watchCurrents(limitQuery, 'resume-token-1004')
|
|
.watchSnapshots(1004)
|
|
.expectActiveTargets({
|
|
query: limitQuery,
|
|
targetPurpose: TargetPurpose.ExistenceFilterMismatch,
|
|
resumeToken: ''
|
|
})
|
|
.watchRemoves(limitQuery)
|
|
.watchAcksFull(limitQuery, 1005, secondDocument)
|
|
// The snapshot after the existence filter mismatch triggers limbo
|
|
// resolution. The local view still contains `firstDocument` and
|
|
// hence we do not yet raise a new snapshot.
|
|
.expectLimboDocs(firstDocument.key)
|
|
.ackLimbo(1006, firstDocumentDeleted)
|
|
.expectLimboDocs()
|
|
// We raise the final snapshot when limbo resolution completes. We now
|
|
// include `secondDocument`, which matches the backend result.
|
|
.expectEvents(limitQuery, {
|
|
added: [secondDocument],
|
|
removed: [firstDocument]
|
|
})
|
|
);
|
|
}
|
|
);
|
|
|
|
specTest('Resumed limit queries use updated documents ', [], () => {
|
|
// This test verifies that a resumed limit query will not contain documents
|
|
// that fell out of the limit while the query was inactive.
|
|
|
|
const limitQuery = queryWithLimit(
|
|
query('collection', orderBy('a')),
|
|
1,
|
|
LimitType.First
|
|
);
|
|
const fullQuery = query('collection', orderBy('a'));
|
|
|
|
const firstDocument = doc('collection/a', 2001, { a: 1 });
|
|
const firstDocumentUpdated = doc('collection/a', 2003, { a: 3 });
|
|
const secondDocument = doc('collection/c', 1000, { a: 2 });
|
|
|
|
return (
|
|
spec()
|
|
.ensureManualLruGC()
|
|
// We issue a limit query with an orderBy constraint.
|
|
.userListens(limitQuery)
|
|
.watchAcksFull(limitQuery, 2001, firstDocument)
|
|
.expectEvents(limitQuery, { added: [firstDocument] })
|
|
.userUnlistens(limitQuery)
|
|
.watchRemoves(limitQuery)
|
|
// We issue a second query which adds `secondDocument` to the cache. We
|
|
// also update `firstDocument` to sort after `secondDocument`.
|
|
// `secondDocument` is now older than `firstDocument` but sorts before it
|
|
// in the limit query.
|
|
.userListens(fullQuery)
|
|
.expectEvents(fullQuery, { added: [firstDocument], fromCache: true })
|
|
.watchAcksFull(fullQuery, 2003, firstDocumentUpdated, secondDocument)
|
|
.expectEvents(fullQuery, {
|
|
added: [secondDocument],
|
|
modified: [firstDocumentUpdated]
|
|
})
|
|
.userUnlistens(fullQuery)
|
|
.watchRemoves(fullQuery)
|
|
// Re-issue the limit query and verify that we return `secondDocument`
|
|
// from cache.
|
|
.userListens(limitQuery, { resumeToken: 'resume-token-2001' })
|
|
.expectEvents(limitQuery, {
|
|
added: [secondDocument],
|
|
fromCache: true
|
|
})
|
|
);
|
|
});
|
|
|
|
specTest('Multiple docs in limbo in full limit query', [], () => {
|
|
const query1 = queryWithLimit(query('collection'), 2, LimitType.First);
|
|
const query2 = query('collection');
|
|
const docA = doc('collection/a', 1000, { key: 'a' });
|
|
const docB = doc('collection/b', 1001, { key: 'b' });
|
|
const docC = doc('collection/c', 1002, { key: 'c' });
|
|
const docD = doc('collection/d', 1003, { key: 'd' });
|
|
const docE = doc('collection/e', 1004, { key: 'e' });
|
|
const docF = doc('collection/f', 1005, { key: 'f' });
|
|
return (
|
|
spec()
|
|
.userListens(query1)
|
|
.watchAcksFull(query1, 1001, docA, docB)
|
|
.expectEvents(query1, {
|
|
added: [docA, docB]
|
|
})
|
|
.userListens(query2)
|
|
.expectEvents(query2, {
|
|
fromCache: true,
|
|
added: [docA, docB]
|
|
})
|
|
.watchAcksFull(query2, 1005, docA, docB, docC, docD, docE, docF)
|
|
.expectEvents(query2, {
|
|
added: [docC, docD, docE, docF]
|
|
})
|
|
.watchResets(query1)
|
|
.watchSends({ affects: [query1] }, docE, docF)
|
|
/**
|
|
* Technically, this sequence can't happen in reality, because the
|
|
* backend can't ack version 2000 without updating the state for C and
|
|
* D, since they are results for query2. query2 is just a hack to make
|
|
* sure that C and D are in the local cache.
|
|
*/
|
|
.watchCurrents(query1, 'resume-token-' + 2000)
|
|
.watchSnapshots(2000)
|
|
.expectLimboDocs(docA.key, docB.key)
|
|
// Limbo document causes query to be "inconsistent"
|
|
.expectEvents(query1, { fromCache: true })
|
|
/**
|
|
* The current implementation resolves limbo documents one at a time.
|
|
* However, if we decide to change this in the future to resolve them
|
|
* all at once, it's okay to change this test to match that.
|
|
*/
|
|
.ackLimbo(2000, deletedDoc('collection/a', 2000))
|
|
.expectLimboDocs(docB.key, docC.key)
|
|
.expectEvents(query2, {
|
|
removed: [docA]
|
|
})
|
|
.expectEvents(query1, {
|
|
fromCache: true,
|
|
added: [docC],
|
|
removed: [docA]
|
|
})
|
|
.watchRemovesLimboTarget(docA)
|
|
.ackLimbo(2001, deletedDoc('collection/b', 2001))
|
|
.expectLimboDocs(docC.key, docD.key)
|
|
.expectEvents(query2, {
|
|
removed: [docB]
|
|
})
|
|
.expectEvents(query1, {
|
|
fromCache: true,
|
|
added: [docD],
|
|
removed: [docB]
|
|
})
|
|
.watchRemovesLimboTarget(docB)
|
|
.ackLimbo(2002, deletedDoc('collection/c', 2002))
|
|
.expectLimboDocs(docD.key)
|
|
.expectEvents(query2, {
|
|
removed: [docC]
|
|
})
|
|
.expectEvents(query1, {
|
|
fromCache: true,
|
|
added: [docE],
|
|
removed: [docC]
|
|
})
|
|
.watchRemovesLimboTarget(docC)
|
|
.ackLimbo(2003, deletedDoc('collection/d', 2003))
|
|
.expectLimboDocs()
|
|
.expectEvents(query2, {
|
|
removed: [docD]
|
|
})
|
|
.expectEvents(query1, {
|
|
added: [docF],
|
|
removed: [docD]
|
|
})
|
|
.watchRemovesLimboTarget(docD)
|
|
);
|
|
});
|
|
|
|
specTest(
|
|
'Limit query is refilled by primary client',
|
|
['multi-client'],
|
|
() => {
|
|
const query1 = queryWithLimit(query('collection'), 2, LimitType.First);
|
|
const doc1 = doc('collection/a', 1000, { key: 'a' });
|
|
const doc2 = doc('collection/b', 1002, { key: 'b' });
|
|
const doc3 = doc('collection/c', 1001, { key: 'c' });
|
|
return client(0)
|
|
.becomeVisible()
|
|
.client(1)
|
|
.userListens(query1)
|
|
.client(0)
|
|
.expectListen(query1)
|
|
.watchAcksFull(query1, 1001, doc1, doc3)
|
|
.client(1)
|
|
.expectEvents(query1, {
|
|
added: [doc1, doc3]
|
|
})
|
|
.client(0)
|
|
.watchSends({ affects: [query1] }, doc2)
|
|
.watchSends({ removed: [query1] }, doc3)
|
|
.watchSnapshots(1002)
|
|
.client(1)
|
|
.expectEvents(query1, {
|
|
added: [doc2],
|
|
removed: [doc3]
|
|
});
|
|
}
|
|
);
|
|
|
|
specTest(
|
|
'Limit query includes write from secondary client ',
|
|
['multi-client'],
|
|
() => {
|
|
const query1 = queryWithLimit(query('collection'), 2, LimitType.First);
|
|
const doc1 = doc('collection/a', 1003, { key: 'a' });
|
|
const doc1Local = doc('collection/a', 0, {
|
|
key: 'a'
|
|
}).setHasLocalMutations();
|
|
const doc2 = doc('collection/b', 1001, { key: 'b' });
|
|
const doc3 = doc('collection/c', 1002, { key: 'c' });
|
|
return client(0)
|
|
.becomeVisible()
|
|
.client(1)
|
|
.userListens(query1)
|
|
.client(0)
|
|
.expectListen(query1)
|
|
.watchAcksFull(query1, 1002, doc2, doc3)
|
|
.client(1)
|
|
.expectEvents(query1, {
|
|
added: [doc2, doc3]
|
|
})
|
|
.client(2)
|
|
.userSets('collection/a', { key: 'a' })
|
|
.client(1)
|
|
.expectEvents(query1, {
|
|
hasPendingWrites: true,
|
|
added: [doc1Local],
|
|
removed: [doc3]
|
|
})
|
|
.client(0)
|
|
.writeAcks('collection/a', 1003, { expectUserCallback: false })
|
|
.watchSends({ affects: [query1] }, doc1)
|
|
.watchSends({ removed: [query1] }, doc3)
|
|
.watchSnapshots(1003)
|
|
.client(1)
|
|
.expectEvents(query1, {
|
|
metadata: [doc1]
|
|
})
|
|
.client(2)
|
|
.expectUserCallbacks({
|
|
acknowledged: ['collection/a']
|
|
});
|
|
}
|
|
);
|
|
});
|