Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LocalDb #969

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Blish HUD/Blish HUD.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@
<PackageReference Include="protobuf-net" Version="3.0.101" PrivateAssets="all" />
<PackageReference Include="System.ComponentModel.Composition" Version="6.0.0" />
<PackageReference Include="System.ServiceModel.Primitives" Version="4.9.0" />
<PackageReference Include="sqlite-net-static" Version="1.9.172" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 3 additions & 1 deletion Blish HUD/GameServices/GameService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public abstract class GameService {
ArcDpsV2 = new ArcDpsServiceV2(), // This needs to be initialized bf the V1
ArcDps = new ArcDpsService(),
Contexts = new ContextsService(),
Module = new ModuleService()
LocalDb = new LocalDbService(),
Module = new ModuleService(),
};

public static IReadOnlyList<GameService> All => _allServices;
Expand Down Expand Up @@ -96,6 +97,7 @@ internal void DoUpdate(GameTime gameTime) {
public static readonly ArcDpsService ArcDps;
public static readonly ArcDpsServiceV2 ArcDpsV2;
public static readonly ContextsService Contexts;
public static readonly LocalDbService LocalDb;
public static readonly ModuleService Module;

#endregion
Expand Down
40 changes: 40 additions & 0 deletions Blish HUD/GameServices/LocalDb/Converters/ApiEnumConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Gw2Sharp.WebApi.V2.Models;
using Newtonsoft.Json;
using System;
using System.Reflection;

#nullable enable

namespace Blish_HUD.LocalDb {
internal class ApiEnumConverter : JsonConverter {
public override bool CanConvert(Type objectType)
=> objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(ApiEnum<>);

public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) {
if (value == null) {
serializer.Serialize(writer, null);
return;
}

var type = value.GetType();
var valueProperty = type.GetProperty("RawValue")
?? throw new JsonSerializationException($"No raw value property found for type {type}");

string? rawValue = (string?)valueProperty.GetValue(value);

serializer.Serialize(writer, rawValue);
}

public override object ReadJson(JsonReader reader, Type objectType, object? existingValue,
JsonSerializer serializer) {
string? rawValue = serializer.Deserialize<string?>(reader);

var constructorInfo = objectType.GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
null, new Type[] { typeof(string) }, null)
?? throw new JsonSerializationException($"No constructor found for type {objectType}");

return constructorInfo.Invoke(new object?[] { rawValue });
}
}
}
50 changes: 50 additions & 0 deletions Blish HUD/GameServices/LocalDb/Converters/ApiFlagsConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Gw2Sharp.WebApi.V2.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Reflection;

#nullable enable

namespace Blish_HUD.LocalDb {
internal class ApiFlagsConverter : JsonConverter {
public override bool CanConvert(Type objectType)
=> objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(ApiFlags<>);

public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) {
if (value == null) {
serializer.Serialize(writer, null);
return;
}

var type = value.GetType();
var listProperty = type.GetProperty("List")
?? throw new JsonSerializationException($"No List property found for type {type}");

object list = listProperty.GetValue(value);

serializer.Serialize(writer, list);
}

public override object ReadJson(JsonReader reader, Type objectType, object? existingValue,
JsonSerializer serializer) {
if (reader.TokenType != JsonToken.StartArray) {
throw new JsonSerializationException("Cannot deserialize ApiFlags");
}

var enumType = objectType.GetGenericArguments()[0];
var apiEnumType = typeof(ApiEnum<>).MakeGenericType(enumType);
var listType = typeof(List<>).MakeGenericType(apiEnumType);

object? rawValue = serializer.Deserialize(reader, listType);

var constructorInfo = objectType.GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
null,
new Type[] { typeof(IEnumerable<>).MakeGenericType(apiEnumType) },
null);

return constructorInfo.Invoke(new object?[] { rawValue });
}
}
}
18 changes: 18 additions & 0 deletions Blish HUD/GameServices/LocalDb/Converters/Coordinates2Converter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Gw2Sharp.Models;
using Newtonsoft.Json;
using System;

#nullable enable

namespace Blish_HUD.LocalDb {
internal class Coordinates2Converter : JsonConverter<Coordinates2> {
public override void WriteJson(JsonWriter writer, Coordinates2 value, JsonSerializer serializer)
=> serializer.Serialize(writer, new double[] { value.X, value.Y });

public override Coordinates2 ReadJson(JsonReader reader, Type objectType, Coordinates2 existingValue,
bool hasExistingValue, JsonSerializer serializer)
=> serializer.Deserialize<double[]>(reader) is double[] { Length: 2 } coords
? new Coordinates2(coords[0], coords[1])
: throw new JsonSerializationException();
}
}
18 changes: 18 additions & 0 deletions Blish HUD/GameServices/LocalDb/Converters/Coordinates3Converter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Gw2Sharp.Models;
using Newtonsoft.Json;
using System;

#nullable enable

namespace Blish_HUD.LocalDb {
internal class Coordinates3Converter : JsonConverter<Coordinates3> {
public override void WriteJson(JsonWriter writer, Coordinates3 value, JsonSerializer serializer)
=> serializer.Serialize(writer, new double[] { value.X, value.Y, value.Z });

public override Coordinates3 ReadJson(JsonReader reader, Type objectType, Coordinates3 existingValue,
bool hasExistingValue, JsonSerializer serializer)
=> serializer.Deserialize<double[]>(reader) is double[] { Length: 3 } coords
? new Coordinates3(coords[0], coords[1], coords[2])
: throw new JsonSerializationException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Gw2Sharp;
using Gw2Sharp.WebApi;
using Newtonsoft.Json;
using System;
using System.Reflection;

#nullable enable

namespace Blish_HUD.LocalDb {
internal class NullableRenderUrlConverter : JsonConverter<RenderUrl?> {
private readonly ConstructorInfo _constructorInfo;

public NullableRenderUrlConverter() {
_constructorInfo = typeof(RenderUrl).GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance, null,
new Type[] { typeof(IGw2Client), typeof(string), typeof(string) }, null) ??
throw new JsonSerializationException($"No matching constructor found for type {typeof(RenderUrl)}");
}

public override void WriteJson(JsonWriter writer, RenderUrl? value, JsonSerializer serializer)
=> serializer.Serialize(writer, value?.Url?.OriginalString);

public override RenderUrl? ReadJson(JsonReader reader, Type objectType, RenderUrl? existingValue,
bool hasExistingValue, JsonSerializer serializer) {
string? url = serializer.Deserialize<string?>(reader);
return (RenderUrl)_constructorInfo.Invoke(new object?[] { null, url, null });
}
}
}
36 changes: 36 additions & 0 deletions Blish HUD/GameServices/LocalDb/Converters/RectangleConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Gw2Sharp.Models;
using Gw2Sharp.WebApi.V2.Models;
using Newtonsoft.Json;
using System;

#nullable enable

namespace Blish_HUD.LocalDb {
internal class RectangleConverter : JsonConverter<Rectangle> {
private class Intermediate {
public Coordinates2 TopLeft;
public Coordinates2 TopRight;
public Coordinates2 BottomLeft;
public Coordinates2 BottomRight;
public RectangleDirectionType Direction;
}

public override void WriteJson(JsonWriter writer, Rectangle value, JsonSerializer serializer)
=> serializer.Serialize(writer, new Intermediate() {
TopLeft = value.TopLeft,
TopRight = value.TopRight,
BottomLeft = value.BottomLeft,
BottomRight = value.BottomRight,
Direction = value.Direction,
});

public override Rectangle ReadJson(JsonReader reader, Type objectType, Rectangle existingValue,
bool hasExistingValue, JsonSerializer serializer)
=> serializer.Deserialize<Intermediate>(reader) is Intermediate i
? new Rectangle(
i.Direction == RectangleDirectionType.BottomUp ? i.BottomLeft : i.TopLeft,
i.Direction == RectangleDirectionType.BottomUp ? i.TopRight : i.BottomRight,
i.Direction)
: throw new JsonSerializationException();
}
}
29 changes: 29 additions & 0 deletions Blish HUD/GameServices/LocalDb/Converters/RenderUrlConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Gw2Sharp;
using Gw2Sharp.WebApi;
using Newtonsoft.Json;
using System;
using System.Reflection;

#nullable enable

namespace Blish_HUD.LocalDb {
internal class RenderUrlConverter : JsonConverter<RenderUrl> {
private readonly ConstructorInfo _constructorInfo;

public RenderUrlConverter() {
_constructorInfo = typeof(RenderUrl).GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance, null,
new Type[] { typeof(IGw2Client), typeof(string), typeof(string) }, null) ??
throw new JsonSerializationException($"No matching constructor found for type {typeof(RenderUrl)}");
}

public override void WriteJson(JsonWriter writer, RenderUrl value, JsonSerializer serializer)
=> serializer.Serialize(writer, value.Url.OriginalString);

public override RenderUrl ReadJson(JsonReader reader, Type objectType, RenderUrl existingValue,
bool hasExistingValue, JsonSerializer serializer) {
string? url = serializer.Deserialize<string>(reader);
return (RenderUrl)_constructorInfo.Invoke(new object?[] { null, url, null });
}
}
}
100 changes: 100 additions & 0 deletions Blish HUD/GameServices/LocalDb/DbHandler.Collection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using SQLite;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

#nullable enable

namespace Blish_HUD.LocalDb {
internal partial class DbHandler {
private interface ILoadCollection : IMetaCollection {
Type IdType { get; }
string TableName { get; }
Version? CurrentVersion { get; }

Task Unload(SQLiteContext db, CancellationToken ct);
Task<bool> Load(SQLiteContext db, CancellationToken ct);
}

private partial class Collection<TId, TItem> : ILoadCollection
where TId : notnull
where TItem : class {
private static readonly Logger _logger = Logger.GetLogger<Collection<TId, TItem>>();

public Type IdType { get; } = typeof(TId);
public string TableName { get; }

public Version? CurrentVersion => _handler._meta.GetVersion(TableName);

public bool IsLoaded => CurrentVersion != null;
public bool IsAvailable => CurrentVersion?.IsValid == true;

private readonly DbHandler _handler;
private readonly Func<CancellationToken, Task<IEnumerable<(TId id, TItem item)>>> _load;

public Collection(
DbHandler handler,
string tableName,
Func<CancellationToken, Task<IEnumerable<(TId id, TItem item)>>> load
) {
_handler = handler;
TableName = tableName;
_load = load;
}

public Task WaitUntilLoaded() {
var tcs = new TaskCompletionSource<object>();

_handler.CollectionLoaded += loaded;

void loaded(IMetaCollection collection) {
if (collection != this) {
return;
}

_handler.CollectionLoaded -= loaded;
tcs.SetResult(null!);
}

return IsLoaded
? Task.CompletedTask
: tcs.Task;
}

public IDbCollection<TId, TItem> Access(SQLiteAsyncConnection db) {
return new LocalCollection<TId, TItem>(db, TableName);
}

public async Task Unload(SQLiteContext db, CancellationToken _) {
await db.Connection.ExecuteAsync($"DELETE FROM `{TableName}`");
}

public async Task<bool> Load(SQLiteContext db, CancellationToken ct) {
try {
var values = await _load(ct);
await db.Connection.RunInTransactionAsync(transaction => {
transaction.ExecuteScalar<string>("PRAGMA locking_mode = EXCLUSIVE");
transaction.ExecuteScalar<string>("PRAGMA journal_mode = MEMORY");

// Delete all content from table first
transaction.Execute($"DELETE FROM `{TableName}`");

// Insert all items into the table
foreach (var (id, value) in values.OrderBy(x => x.id)) {
transaction.Execute(
$"INSERT INTO `{TableName}` ({SQLiteContext.ID_COLUMN}, {SQLiteContext.DATA_COLUMN})\n" +
$"VALUES (?, jsonb(?))", id, SQLiteContext.Serialize(value));
}
});

return true;
} catch (Exception e) {
_logger.Warn(e, $"Failed to load cache for {TableName}");
return false;
}
}
}
}
}
Loading