Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 0868a1a

Browse files
committed
Merge master into feature/login-on-sync-page
2 parents a10b917 + b2456d8 commit 0868a1a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+4280
-126
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,5 @@ AkavacheSqliteLinkerOverride.cs
234234
NuGetBuild
235235
WiX.Toolset.DummyFile.txt
236236
nunit-UnitTests.xml
237+
nunit-TrackingCollectionTests.xml
237238
GitHubVS.sln.DotSettings

GitHubVS.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialManagement", "src
102102
EndProject
103103
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Testing_Net45", "submodules\reactiveui\ReactiveUI.Testing\ReactiveUI.Testing_Net45.csproj", "{DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}"
104104
EndProject
105+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrackingCollectionTests", "src\TrackingCollectionTests\TrackingCollectionTests.csproj", "{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}"
106+
EndProject
105107
Global
106108
GlobalSection(SolutionConfigurationPlatforms) = preSolution
107109
Debug|Any CPU = Debug|Any CPU
@@ -401,6 +403,18 @@ Global
401403
{DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Release|Any CPU.Build.0 = Release|Any CPU
402404
{DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Release|x86.ActiveCfg = Release|Any CPU
403405
{DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Release|x86.Build.0 = Release|Any CPU
406+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
407+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
408+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Debug|x86.ActiveCfg = Debug|Any CPU
409+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Debug|x86.Build.0 = Debug|Any CPU
410+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Publish|Any CPU.ActiveCfg = Release|Any CPU
411+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Publish|Any CPU.Build.0 = Release|Any CPU
412+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Publish|x86.ActiveCfg = Release|Any CPU
413+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Publish|x86.Build.0 = Release|Any CPU
414+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
415+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Release|Any CPU.Build.0 = Release|Any CPU
416+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Release|x86.ActiveCfg = Release|Any CPU
417+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Release|x86.Build.0 = Release|Any CPU
404418
EndGlobalSection
405419
GlobalSection(SolutionProperties) = preSolution
406420
HideSolutionNode = FALSE
@@ -427,5 +441,6 @@ Global
427441
{252CE1C2-027A-4445-A3C2-E4D6C80A935A} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AF9}
428442
{0EC8DBA1-D745-4EE5-993A-6026440EC3BF} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AF9}
429443
{DD99FD0F-82F6-4C30-930E-4A1D0DF01D65} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB9}
444+
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD}
430445
EndGlobalSection
431446
EndGlobal

script

src/GitHub.App/Api/ApiClient.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Reactive.Linq;
88
using System.Security.Cryptography;
99
using System.Text;
10-
using GitHub.Authentication;
1110
using GitHub.Primitives;
1211
using NLog;
1312
using NullGuard;
@@ -122,8 +121,6 @@ public IObservable<LicenseMetadata> GetLicenses()
122121

123122
public HostAddress HostAddress { get; }
124123

125-
public ITwoFactorChallengeHandler TwoFactorChallengeHandler { get; private set; }
126-
127124
static string GetSha256Hash(string input)
128125
{
129126
try
@@ -198,5 +195,15 @@ public IObservable<Unit> DeleteApplicationAuthorization(int id, [AllowNull]strin
198195
{
199196
return gitHubClient.Authorization.Delete(id, twoFactorAuthorizationCode);
200197
}
198+
199+
public IObservable<PullRequest> GetPullRequestsForRepository(string owner, string name)
200+
{
201+
return gitHubClient.PullRequest.GetAllForRepository(owner, name,
202+
new PullRequestRequest {
203+
State = ItemState.All,
204+
SortProperty = PullRequestSort.Updated,
205+
SortDirection = SortDirection.Descending
206+
});
207+
}
201208
}
202209
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.Linq;
5+
using System.Reactive.Linq;
6+
using Akavache;
7+
using NullGuard;
8+
9+
namespace GitHub.Caches
10+
{
11+
public class CacheIndex
12+
{
13+
public static CacheIndex Create(string key)
14+
{
15+
return new CacheIndex { IndexKey = key };
16+
}
17+
18+
public CacheIndex()
19+
{
20+
Keys = new List<string>();
21+
OldKeys = new List<string>();
22+
}
23+
24+
public IObservable<CacheIndex> AddAndSave(IBlobCache cache, string indexKey, CacheItem item,
25+
DateTimeOffset? absoluteExpiration = null)
26+
{
27+
var k = string.Format(CultureInfo.InvariantCulture, "{0}|{1}", IndexKey, item.Key);
28+
if (!Keys.Contains(k))
29+
Keys.Add(k);
30+
UpdatedAt = DateTimeOffset.UtcNow;
31+
return cache.InsertObject(IndexKey, this, absoluteExpiration)
32+
.Select(x => this);
33+
}
34+
35+
public static IObservable<CacheIndex> AddAndSaveToIndex(IBlobCache cache, string indexKey, CacheItem item,
36+
DateTimeOffset? absoluteExpiration = null)
37+
{
38+
return cache.GetOrCreateObject(indexKey, () => Create(indexKey))
39+
.Do(index =>
40+
{
41+
var k = string.Format(CultureInfo.InvariantCulture, "{0}|{1}", index.IndexKey, item.Key);
42+
if (!index.Keys.Contains(k))
43+
index.Keys.Add(k);
44+
index.UpdatedAt = DateTimeOffset.UtcNow;
45+
})
46+
.SelectMany(index => cache.InsertObject(index.IndexKey, index, absoluteExpiration)
47+
.Select(x => index));
48+
}
49+
50+
public IObservable<CacheIndex> Clear(IBlobCache cache, string indexKey, DateTimeOffset? absoluteExpiration = null)
51+
{
52+
OldKeys = Keys.ToList();
53+
Keys.Clear();
54+
UpdatedAt = DateTimeOffset.UtcNow;
55+
return cache
56+
.InvalidateObject<CacheIndex>(indexKey)
57+
.SelectMany(_ => cache.InsertObject(indexKey, this, absoluteExpiration))
58+
.Select(_ => this);
59+
}
60+
61+
[AllowNull]
62+
public string IndexKey {[return: AllowNull] get; set; }
63+
public List<string> Keys { get; set; }
64+
public DateTimeOffset UpdatedAt { get; set; }
65+
public List<string> OldKeys { get; set; }
66+
}
67+
}

src/GitHub.App/Caches/CacheItem.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Reactive.Linq;
4+
using Akavache;
5+
using NullGuard;
6+
7+
namespace GitHub.Caches
8+
{
9+
public class CacheItem
10+
{
11+
[AllowNull]
12+
public string Key {[return: AllowNull] get; set; }
13+
public DateTimeOffset Timestamp { get; set; }
14+
15+
public IObservable<T> Save<T>(IBlobCache cache, string key, DateTimeOffset? absoluteExpiration = null)
16+
where T : CacheItem
17+
{
18+
var k = string.Format(CultureInfo.InvariantCulture, "{0}|{1}", key, Key);
19+
return cache
20+
.InvalidateObject<T>(k)
21+
.Select(_ => cache.InsertObject(k, this, absoluteExpiration))
22+
.Select(_ => this as T);
23+
}
24+
}
25+
}

src/GitHub.App/Extensions/AkavacheExtensions.cs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using System;
2+
using System.Linq;
3+
using System.Reactive;
24
using System.Reactive.Linq;
35
using Akavache;
6+
using GitHub.Caches;
47

58
namespace GitHub.Extensions
69
{
@@ -14,6 +17,7 @@ public static class AkavacheExtensions
1417
/// the stale value will be produced first, followed by the fresh value
1518
/// when the fetch func completes.
1619
/// </summary>
20+
/// <param name="blobCache">The cache to retrieve the object from.</param>
1721
/// <param name="key">The key to look up the cache value with.</param>
1822
/// <param name="fetchFunc">The fetch function.</param>
1923
/// <param name="refreshInterval">
@@ -59,6 +63,7 @@ public static IObservable<T> GetAndRefreshObject<T>(
5963
/// the stale value will be produced first, followed by the fresh value
6064
/// when the fetch func completes.
6165
/// </summary>
66+
/// <param name="blobCache">The cache to retrieve the object from.</param>
6267
/// <param name="key">The key to look up the cache value with.</param>
6368
/// <param name="fetchFunc">The fetch function.</param>
6469
/// <param name="refreshInterval">
@@ -133,10 +138,105 @@ static IObservable<byte[]> GetAndFetchLatestBytes(
133138
.RefCount();
134139
}
135140

141+
/// <summary>
142+
/// This method attempts to return a series of cached value(s) aggregated by
143+
/// a <paramref name="key"/>. In the case of a
144+
/// cache miss the fetchFunc will be used to provide a fresh value which
145+
/// is then returned. In the case of a cache hit where the cache item is
146+
/// considered stale (but not expired) as determined by <paramref name="refreshInterval"/>
147+
/// the stale values will be produced first, followed by the fresh values
148+
/// when the fetch func completes.
149+
/// </summary>
150+
/// <param name="blobCache">The cache to retrieve the object from.</param>
151+
/// <param name="key">The key to look up the cache value with.</param>
152+
/// <param name="fetchFunc">The fetch function.</param>
153+
/// <param name="removedItemsCallback">The callback that receives items that
154+
/// were a part of the cache but not of the list list of values.</param>
155+
/// <param name="refreshInterval">
156+
/// Cache objects with an age exceeding this value will be treated as stale
157+
/// and the fetch function will be invoked to refresh it.
158+
/// </param>
159+
/// <param name="maxCacheDuration">
160+
/// The maximum age of a cache object before the object is treated as
161+
/// expired and unusable. Cache objects older than this will be treated
162+
/// as a cache miss.
163+
/// </param>
164+
public static IObservable<T> GetAndFetchLatestFromIndex<T>(
165+
this IBlobCache blobCache,
166+
string key,
167+
Func<IObservable<T>> fetchFunc,
168+
Action<T> removedItemsCallback,
169+
TimeSpan refreshInterval,
170+
TimeSpan maxCacheDuration)
171+
where T : CacheItem
172+
{
173+
return Observable.Defer(() =>
174+
{
175+
var absoluteExpiration = blobCache.Scheduler.Now + maxCacheDuration;
176+
177+
try
178+
{
179+
return blobCache.GetAndFetchLatestFromIndex(
180+
key,
181+
fetchFunc,
182+
removedItemsCallback,
183+
createdAt => IsExpired(blobCache, createdAt, refreshInterval),
184+
absoluteExpiration);
185+
}
186+
catch (Exception exc)
187+
{
188+
return Observable.Throw<T>(exc);
189+
}
190+
});
191+
}
192+
193+
static IObservable<T> GetAndFetchLatestFromIndex<T>(this IBlobCache This,
194+
string key,
195+
Func<IObservable<T>> fetchFunc,
196+
Action<T> removedItemsCallback,
197+
Func<DateTimeOffset, bool> fetchPredicate = null,
198+
DateTimeOffset? absoluteExpiration = null,
199+
bool shouldInvalidateOnError = false)
200+
where T : CacheItem
201+
{
202+
var fetch = Observable.Defer(() => This.GetOrCreateObject(key, () => CacheIndex.Create(key))
203+
.Select(x => Tuple.Create(x, fetchPredicate == null || !x.Keys.Any() || fetchPredicate(x.UpdatedAt)))
204+
.Where(predicateIsTrue => predicateIsTrue.Item2)
205+
.Select(x => x.Item1)
206+
.SelectMany(index => index.Clear(This, key, absoluteExpiration))
207+
.SelectMany(index =>
208+
{
209+
var fetchObs = fetchFunc().Catch<T, Exception>(ex =>
210+
{
211+
var shouldInvalidate = shouldInvalidateOnError ?
212+
This.InvalidateObject<CacheIndex>(key) :
213+
Observable.Return(Unit.Default);
214+
return shouldInvalidate.SelectMany(__ => Observable.Throw<T>(ex));
215+
});
216+
217+
return fetchObs
218+
.SelectMany(x => x.Save<T>(This, key, absoluteExpiration))
219+
.Do(x => index.AddAndSave(This, key, x, absoluteExpiration))
220+
.Finally(() =>
221+
{
222+
This.GetObjects<T>(index.OldKeys.Except(index.Keys))
223+
.Do(dict => This.InvalidateObjects<T>(dict.Keys))
224+
.SelectMany(dict => dict.Values)
225+
.Do(removedItemsCallback)
226+
.Subscribe();
227+
});
228+
}));
229+
230+
var cache = Observable.Defer(() => This.GetOrCreateObject(key, () => CacheIndex.Create(key))
231+
.SelectMany(index => This.GetObjects<T>(index.Keys))
232+
.SelectMany(dict => dict.Values));
233+
234+
return cache.Merge(fetch).Replay().RefCount();
235+
}
236+
136237
static bool IsExpired(IBlobCache blobCache, DateTimeOffset itemCreatedAt, TimeSpan cacheDuration)
137238
{
138-
var now = blobCache.Scheduler.Now;
139-
var elapsed = now - itemCreatedAt;
239+
var elapsed = blobCache.Scheduler.Now - itemCreatedAt.ToUniversalTime();
140240

141241
return elapsed > cacheDuration;
142242
}

src/GitHub.App/GitHub.App.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,15 @@
123123
<None Include="..\..\script\Key.snk" Condition="$(Buildtype) == 'Internal'">
124124
<Link>Key.snk</Link>
125125
</None>
126+
<Compile Include="Caches\CacheIndex.cs" />
127+
<Compile Include="Caches\CacheItem.cs" />
126128
<Compile Include="Caches\ImageCache.cs" />
127129
<Compile Include="Extensions\AkavacheExtensions.cs" />
128130
<Compile Include="Extensions\EnvironmentExtensions.cs" />
129131
<Compile Include="Extensions\ValidationExtensions.cs" />
130132
<Compile Include="GlobalSuppressions.cs" />
131133
<Compile Include="Infrastructure\LoggingConfiguration.cs" />
134+
<Compile Include="Models\PullRequestModel.cs" />
132135
<Compile Include="Resources.Designer.cs">
133136
<AutoGen>True</AutoGen>
134137
<DesignTime>True</DesignTime>

0 commit comments

Comments
 (0)