Files

260 lines
8.5 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 } from '../../../src/core/query';
import { TimerId } from '../../../src/util/async_queue';
import { Code } from '../../../src/util/error';
import { doc, query } from '../../util/helpers';
import { describeSpec, specTest } from './describe_spec';
import { spec } from './spec_builder';
describeSpec('Offline:', [], () => {
specTest('Empty queries are resolved if client goes offline', [], () => {
const query1 = query('collection');
return (
spec()
.userListens(query1)
.watchStreamCloses(Code.UNAVAILABLE)
.expectEvents(query1, {
fromCache: true,
hasPendingWrites: false
})
// no further events
.watchStreamCloses(Code.UNAVAILABLE)
.watchStreamCloses(Code.UNAVAILABLE)
);
});
specTest('A successful message delays offline status', [], () => {
const query1 = query('collection');
return (
spec()
.userListens(query1)
.watchAcks(query1)
// first error triggers unknown state
.watchStreamCloses(Code.UNAVAILABLE)
// second error triggers offline state
.watchStreamCloses(Code.UNAVAILABLE)
.expectEvents(query1, {
fromCache: true,
hasPendingWrites: false
})
// no further events
.watchStreamCloses(Code.UNAVAILABLE)
.watchStreamCloses(Code.UNAVAILABLE)
);
});
specTest(
'Removing all listeners delays "Offline" status on next listen',
['eager-gc'],
'Marked as no-lru because when a listen is re-added, it gets a new target id rather than ' +
'reusing one',
() => {
const query1 = query('collection');
return (
spec()
.userListens(query1)
// error triggers offline state
.watchStreamCloses(Code.UNAVAILABLE)
.expectEvents(query1, {
fromCache: true,
hasPendingWrites: false
})
// Remove listen.
.userUnlistens(query1)
// If the next (already scheduled) connection attempt fails, we'll move
// to unknown since there are no listeners, and stop trying to connect.
.watchStreamCloses(Code.UNAVAILABLE)
// Suppose sometime later we listen again, it should take one failure
// before we get cached data.
.userListens(query1)
.watchStreamCloses(Code.UNAVAILABLE)
.expectEvents(query1, {
fromCache: true,
hasPendingWrites: false
})
);
}
);
specTest('Queries revert to fromCache=true when offline.', [], () => {
const query1 = query('collection');
const docA = doc('collection/a', 1000, { key: 'a' });
return (
spec()
.userListens(query1)
.watchAcksFull(query1, 1000, docA)
.expectEvents(query1, { added: [docA] })
// first error triggers unknown state
.watchStreamCloses(Code.UNAVAILABLE)
.restoreListen(query1, 'resume-token-1000')
// second error triggers offline state and fromCache: true
.watchStreamCloses(Code.UNAVAILABLE)
.expectEvents(query1, { fromCache: true })
// Going online and getting a CURRENT message triggers fromCache: false
.watchAcksFull(query1, 1000)
.expectEvents(query1, { fromCache: false })
);
});
specTest('Queries with limbo documents handle going offline.', [], () => {
const query1 = query('collection');
const docA = doc('collection/a', 1000, { key: 'a' });
const limboQuery = newQueryForPath(docA.key.path);
return (
spec()
.userListens(query1)
.watchAcksFull(query1, 1000, docA)
.expectEvents(query1, { added: [docA] })
.watchResets(query1)
// No more documents
.watchCurrents(query1, 'resume-token-1001')
.watchSnapshots(1001)
// docA will now be in limbo (triggering fromCache=true)
.expectLimboDocs(docA.key)
.expectEvents(query1, { fromCache: true })
// first error triggers unknown state
.watchStreamCloses(Code.UNAVAILABLE)
.restoreListen(query1, 'resume-token-1001')
// second error triggers offline state.
.watchStreamCloses(Code.UNAVAILABLE)
.watchAcksFull(query1, 1001)
.watchAcksFull(limboQuery, 1001)
// Limbo document is resolved. No longer from cache.
.expectEvents(query1, { removed: [docA], fromCache: false })
.expectLimboDocs()
);
});
specTest('OnlineState timeout triggers offline behavior', [], () => {
const query1 = query('collection');
const docA = doc('collection/a', 1000, { key: 'a' });
return (
spec()
.userListens(query1)
// OnlineState timer should trigger offline behavior (fromCache=true).
.runTimer(TimerId.OnlineStateTimeout)
.expectEvents(query1, {
fromCache: true
})
// We should get no further events for failed connection attempts.
.watchStreamCloses(Code.UNAVAILABLE)
.watchStreamCloses(Code.UNAVAILABLE)
// We should get events after a successful connection.
.watchAcksFull(query1, 1000, docA)
.expectEvents(query1, { added: [docA], fromCache: false })
// Running timers should have no effect now.
.runTimer(TimerId.All)
// After a disconnect, the timer should become active again.
.watchStreamCloses(Code.UNAVAILABLE)
.restoreListen(query1, 'resume-token-1000')
.runTimer(TimerId.OnlineStateTimeout)
.expectEvents(query1, {
fromCache: true
})
);
});
specTest(
'New queries return immediately with fromCache=true when offline due to ' +
'stream failures.',
[],
() => {
const query1 = query('collection');
const query2 = query('collection2');
return (
spec()
.userListens(query1)
// After failure, we mark the client offline and trigger an empty
// fromCache event.
.watchStreamCloses(Code.UNAVAILABLE)
.expectEvents(query1, { fromCache: true })
// A new query should immediately return from cache.
.userListens(query2)
.expectEvents(query2, { fromCache: true })
);
}
);
specTest(
'New queries return immediately with fromCache=true when offline due to ' +
'OnlineState timeout.',
[],
() => {
const query1 = query('collection');
const query2 = query('collection2');
return (
spec()
.userListens(query1)
.runTimer(TimerId.OnlineStateTimeout)
.expectEvents(query1, { fromCache: true })
// A new query should immediately return from cache.
.userListens(query2)
.expectEvents(query2, { fromCache: true })
);
}
);
// TODO(b/114055812): This shouldn't really need to be marked eager-gc
specTest(
'Queries return from cache when network disabled',
['eager-gc'],
() => {
const query1 = query('collection');
return (
spec()
.disableNetwork()
.userListens(query1)
.expectEvents(query1, { fromCache: true })
.userUnlistens(query1)
// There was once a bug where removing the last listener accidentally
// reverted us to OnlineState.Unknown, so make sure it works a second time
.userListens(query1)
.expectEvents(query1, { fromCache: true })
.userUnlistens(query1)
);
}
);
specTest('Client stays offline during credential change', [], () => {
// Reproduces a bug that caused the client to switch to OnlineState
// `Unknown` during a credential change.
const query1 = query('collection');
return (
spec()
.disableNetwork()
.changeUser('user1')
.userListens(query1)
// Client is still offline and we raise a `fromCache` event immediately
.expectEvents(query1, {
fromCache: true
})
);
});
});