|
16 | 16 | */
|
17 | 17 |
|
18 | 18 | import { Query } from '../../../src/core/query';
|
19 |
| -import { deletedDoc, doc, filter, path } from '../../util/helpers'; |
| 19 | +import { deletedDoc, doc, filter, orderBy, path } from '../../util/helpers'; |
20 | 20 |
|
21 | 21 | import { describeSpec, specTest } from './describe_spec';
|
22 | 22 | import { client, spec } from './spec_builder';
|
@@ -184,6 +184,130 @@ describeSpec('Limits:', [], () => {
|
184 | 184 | });
|
185 | 185 | });
|
186 | 186 |
|
| 187 | + specTest( |
| 188 | + 'Resumed limit queries exclude deleted documents ', |
| 189 | + ['durable-persistence'], |
| 190 | + () => { |
| 191 | + // This test verifies that views for limit queries are updated even |
| 192 | + // when documents are deleted while the query is inactive. |
| 193 | + |
| 194 | + const limitQuery = Query.atPath(path('collection')) |
| 195 | + .addOrderBy(orderBy('a')) |
| 196 | + .withLimit(1); |
| 197 | + const fullQuery = Query.atPath(path('collection')).addOrderBy( |
| 198 | + orderBy('a') |
| 199 | + ); |
| 200 | + |
| 201 | + const firstDocument = doc('collection/a', 1001, { a: 1 }); |
| 202 | + const firstDocumentDeleted = deletedDoc('collection/a', 1003); |
| 203 | + const secondDocument = doc('collection/b', 1000, { a: 2 }); |
| 204 | + |
| 205 | + return ( |
| 206 | + spec() |
| 207 | + .withGCEnabled(false) |
| 208 | + .userListens(limitQuery) |
| 209 | + .watchAcksFull(limitQuery, 1001, firstDocument) |
| 210 | + .expectEvents(limitQuery, { added: [firstDocument] }) |
| 211 | + .userUnlistens(limitQuery) |
| 212 | + .watchRemoves(limitQuery) |
| 213 | + .userListens(fullQuery) |
| 214 | + .expectEvents(fullQuery, { added: [firstDocument], fromCache: true }) |
| 215 | + .watchAcksFull(fullQuery, 1002, firstDocument, secondDocument) |
| 216 | + .expectEvents(fullQuery, { added: [secondDocument] }) |
| 217 | + // Another client modified `firstDocument` and we lost access to it. |
| 218 | + // Watch sends us a remove, but doesn't tell us the new document state. |
| 219 | + // Since we don't know the state of the document, we mark it as limbo. |
| 220 | + .watchRemovesDoc(firstDocument.key, fullQuery) |
| 221 | + .watchSnapshots(1003) |
| 222 | + .expectEvents(fullQuery, { fromCache: true }) |
| 223 | + .expectLimboDocs(firstDocument.key) |
| 224 | + .userUnlistens(fullQuery) |
| 225 | + // Since we stop listening to `fullQuery`, we disregard our attempt to |
| 226 | + // resolve the limbo state of `firstDocument`. |
| 227 | + .expectLimboDocs() |
| 228 | + .watchRemoves(fullQuery) |
| 229 | + // We restart the client, which clears the limbo target mapping in the |
| 230 | + // spec test runner. Without restarting, the runner assumes that each |
| 231 | + // limbo document is always assigned the same target ID. SyncEngine, |
| 232 | + // however, uses new target IDs if a document goes in and out of limbo. |
| 233 | + .restart() |
| 234 | + // We listen to the limit query again. Note that we include |
| 235 | + // `firstDocument` in the local result since we did not resolve its |
| 236 | + // limbo state. |
| 237 | + .userListens(limitQuery, 'resume-token-1001') |
| 238 | + .expectEvents(limitQuery, { added: [firstDocument], fromCache: true }) |
| 239 | + .watchAcks(limitQuery) |
| 240 | + // Watch resumes the query from the provided resume token, but does |
| 241 | + // not guarantee to send us the removal of `firstDocument`. Instead, |
| 242 | + // we receive an existence filter, which indicates that our view is |
| 243 | + // out of sync. |
| 244 | + .watchSends({ affects: [limitQuery] }, secondDocument) |
| 245 | + .watchFilters([limitQuery], secondDocument.key) |
| 246 | + .watchSnapshots(1004) |
| 247 | + .expectActiveTargets({ query: limitQuery, resumeToken: '' }) |
| 248 | + .watchRemoves(limitQuery) |
| 249 | + .watchAcksFull(limitQuery, 1005, secondDocument) |
| 250 | + // The snapshot after the existence filter mismatch triggers limbo |
| 251 | + // resolution. The local view still contains `firstDocument` and |
| 252 | + // hence we do not yet raise a new snapshot. |
| 253 | + .expectLimboDocs(firstDocument.key) |
| 254 | + .ackLimbo(1006, firstDocumentDeleted) |
| 255 | + .expectLimboDocs() |
| 256 | + // We raise the final snapshot when limbo resolution completes. We now |
| 257 | + // include `secondDocument`, which matches the backend result. |
| 258 | + .expectEvents(limitQuery, { |
| 259 | + added: [secondDocument], |
| 260 | + removed: [firstDocument] |
| 261 | + }) |
| 262 | + ); |
| 263 | + } |
| 264 | + ); |
| 265 | + |
| 266 | + specTest('Resumed limit queries use updated documents ', [], () => { |
| 267 | + // This test verifies that a resumed limit query will not contain documents |
| 268 | + // that fell out of the limit while the query was inactive. |
| 269 | + |
| 270 | + const limitQuery = Query.atPath(path('collection')) |
| 271 | + .addOrderBy(orderBy('a')) |
| 272 | + .withLimit(1); |
| 273 | + const fullQuery = Query.atPath(path('collection')).addOrderBy(orderBy('a')); |
| 274 | + |
| 275 | + const firstDocument = doc('collection/a', 2001, { a: 1 }); |
| 276 | + const firstDocumentUpdated = doc('collection/a', 2003, { a: 3 }); |
| 277 | + const secondDocument = doc('collection/c', 1000, { a: 2 }); |
| 278 | + |
| 279 | + return ( |
| 280 | + spec() |
| 281 | + .withGCEnabled(false) |
| 282 | + // We issue a limit query with an orderBy constraint. |
| 283 | + .userListens(limitQuery) |
| 284 | + .watchAcksFull(limitQuery, 2001, firstDocument) |
| 285 | + .expectEvents(limitQuery, { added: [firstDocument] }) |
| 286 | + .userUnlistens(limitQuery) |
| 287 | + .watchRemoves(limitQuery) |
| 288 | + // We issue a second query which adds `secondDocument` to the cache. We |
| 289 | + // also update `firstDocument` to sort after `secondDocument`. |
| 290 | + // `secondDocument` is now older than `firstDocument` but sorts before it |
| 291 | + // in the limit query. |
| 292 | + .userListens(fullQuery) |
| 293 | + .expectEvents(fullQuery, { added: [firstDocument], fromCache: true }) |
| 294 | + .watchAcksFull(fullQuery, 2003, firstDocumentUpdated, secondDocument) |
| 295 | + .expectEvents(fullQuery, { |
| 296 | + added: [secondDocument], |
| 297 | + modified: [firstDocumentUpdated] |
| 298 | + }) |
| 299 | + .userUnlistens(fullQuery) |
| 300 | + .watchRemoves(fullQuery) |
| 301 | + // Re-issue the limit query and verify that we return `secondDocument` |
| 302 | + // from cache. |
| 303 | + .userListens(limitQuery, 'resume-token-2001') |
| 304 | + .expectEvents(limitQuery, { |
| 305 | + added: [secondDocument], |
| 306 | + fromCache: true |
| 307 | + }) |
| 308 | + ); |
| 309 | + }); |
| 310 | + |
187 | 311 | specTest('Multiple docs in limbo in full limit query', [], () => {
|
188 | 312 | const query1 = Query.atPath(path('collection')).withLimit(2);
|
189 | 313 | const query2 = Query.atPath(path('collection'));
|
|
0 commit comments