cache assemblies in IndexedDB on WebAssembly

This commit is contained in:
Shaun Walker 2022-09-12 14:46:46 -04:00
parent 2d306e8fda
commit b8e2c729c1
4 changed files with 296 additions and 39 deletions

View File

@ -7,16 +7,19 @@ using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI;
using static System.Net.WebRequestMethods;
namespace Oqtane.Client
{
@ -42,7 +45,9 @@ namespace Oqtane.Client
// register scoped core services
builder.Services.AddOqtaneScopedServices();
await LoadClientAssemblies(httpClient);
var serviceProvider = builder.Services.BuildServiceProvider();
await LoadClientAssemblies(httpClient, serviceProvider);
var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies();
foreach (var assembly in assemblies)
@ -54,25 +59,84 @@ namespace Oqtane.Client
RegisterClientStartups(assembly, builder.Services);
}
var host = builder.Build();
await SetCultureFromLocalizationCookie(host.Services);
ServiceActivator.Configure(host.Services);
await host.RunAsync();
await builder.Build().RunAsync();
}
private static async Task LoadClientAssemblies(HttpClient http)
private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider serviceProvider)
{
// get list of loaded assemblies on the client
var assemblies = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList();
// get assemblies from server and load into client app domain
var zip = await http.GetByteArrayAsync($"/api/Installation/load");
var dlls = new Dictionary<string, byte[]>();
var pdbs = new Dictionary<string, byte[]>();
var filter = new List<string>();
var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();
var interop = new Interop(jsRuntime);
var files = await interop.GetIndexedDBKeys(".dll");
if (files.Count() != 0)
{
// get list of assemblies from server
var json = await http.GetStringAsync("/api/Installation/list");
var assemblies = JsonSerializer.Deserialize<List<string>>(json);
// determine which assemblies need to be downloaded
foreach (var assembly in assemblies)
{
var file = files.FirstOrDefault(item => item.Contains(assembly));
if (file == null)
{
filter.Add(assembly);
}
else
{
// check if newer version available
if (GetFileDate(assembly) > GetFileDate(file))
{
filter.Add(assembly);
}
}
}
// get assemblies already downloaded
foreach (var file in files)
{
if (assemblies.Contains(file) && !filter.Contains(file))
{
try
{
dlls.Add(file, await interop.GetIndexedDBItem<byte[]>(file));
var pdb = file.Replace(".dll", ".pdb");
if (files.Contains(pdb))
{
pdbs.Add(pdb, await interop.GetIndexedDBItem<byte[]>(pdb));
}
}
catch
{
// ignore
}
}
else // file is deprecated
{
try
{
await interop.RemoveIndexedDBItem(file);
}
catch
{
// ignore
}
}
}
}
else
{
filter.Add("*");
}
if (filter.Count != 0)
{
// get assemblies from server and load into client app domain
var zip = await http.GetByteArrayAsync($"/api/Installation/load?list=" + string.Join(",", filter));
// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
@ -83,6 +147,17 @@ namespace Oqtane.Client
{
entry.Open().CopyTo(memoryStream);
byte[] file = memoryStream.ToArray();
// save assembly to indexeddb
try
{
await interop.SetIndexedDBItem(entry.FullName, file);
}
catch
{
// ignore
}
switch (Path.GetExtension(entry.FullName))
{
case ".dll":
@ -95,6 +170,7 @@ namespace Oqtane.Client
}
}
}
}
// load assemblies into app domain
foreach (var item in dlls)
@ -110,6 +186,12 @@ namespace Oqtane.Client
}
}
private static DateTime GetFileDate(string filepath)
{
var segments = filepath.Split('.');
return DateTime.ParseExact(segments[segments.Length - 2], "yyyyMMddHHmmss", CultureInfo.InvariantCulture);
}
private static void RegisterModuleServices(Assembly assembly, IServiceCollection services)
{
// dynamically register module scoped services

View File

@ -1,6 +1,12 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Net;
using System;
using System.Threading.Tasks;
using System.Text.Json;
using System.Xml.Linq;
using System.Collections.Generic;
using System.Linq;
namespace Oqtane.UI
{
@ -307,5 +313,76 @@ namespace Oqtane.UI
return new ValueTask<int>(-1);
}
}
public Task SetIndexedDBItem(string key, object value)
{
try
{
_jsRuntime.InvokeVoidAsync(
"Oqtane.Interop.setIndexedDBItem",
key, value);
return Task.CompletedTask;
}
catch
{
return Task.CompletedTask;
}
}
public async Task<T> GetIndexedDBItem<T>(string key)
{
try
{
return await _jsRuntime.InvokeAsync<T>(
"Oqtane.Interop.getIndexedDBItem",
key);
}
catch
{
return default(T);
}
}
public async Task<List<string>> GetIndexedDBKeys()
{
return await GetIndexedDBKeys("");
}
public async Task<List<string>> GetIndexedDBKeys(string contains)
{
try
{
var items = await _jsRuntime.InvokeAsync<JsonDocument>(
"Oqtane.Interop.getIndexedDBKeys");
if (!string.IsNullOrEmpty(contains))
{
return items.Deserialize<List<string>>()
.Where(item => item.Contains(contains)).ToList();
}
else
{
return items.Deserialize<List<string>>();
}
}
catch
{
return new List<string>();
}
}
public Task RemoveIndexedDBItem(string key)
{
try
{
_jsRuntime.InvokeVoidAsync(
"Oqtane.Interop.removeIndexedDBItem",
key);
return Task.CompletedTask;
}
catch
{
return Task.CompletedTask;
}
}
}
}

View File

@ -72,13 +72,14 @@ public static class MauiProgram
var dlls = new Dictionary<string, byte[]>();
var pdbs = new Dictionary<string, byte[]>();
var filter = new List<string>();
var files = new List<string>();
foreach (var file in Directory.EnumerateFiles(folder, "*.dll", SearchOption.AllDirectories))
{
files.Add(file.Substring(folder.Length + 1).Replace("\\", "/"));
}
if (files.Count() != 0)
{
// get list of assemblies from server
@ -107,6 +108,8 @@ public static class MauiProgram
foreach (var file in files)
{
if (assemblies.Contains(file) && !filter.Contains(file))
{
try
{
dlls.Add(file, File.ReadAllBytes(Path.Combine(folder, file)));
var pdb = file.Replace(".dll", ".pdb");
@ -115,10 +118,22 @@ public static class MauiProgram
pdbs.Add(pdb, File.ReadAllBytes(Path.Combine(folder, pdb)));
}
}
catch
{
// ignore
}
}
else // file is deprecated
{
try
{
File.Delete(Path.Combine(folder, file));
}
catch
{
// ignore
}
}
}
}
else
@ -142,6 +157,8 @@ public static class MauiProgram
byte[] file = memoryStream.ToArray();
// save assembly to local folder
try
{
int subfolder = entry.FullName.IndexOf('/');
if (subfolder != -1 && !Directory.Exists(Path.Combine(folder, entry.FullName.Substring(0, subfolder))))
{
@ -149,6 +166,11 @@ public static class MauiProgram
}
using var stream = File.Create(Path.Combine(folder, entry.FullName));
stream.Write(file, 0, file.Length);
}
catch
{
// ignore
}
if (Path.GetExtension(entry.FullName) == ".dll")
{

View File

@ -395,5 +395,81 @@ Oqtane.Interop = {
getCaretPosition: function (id) {
var element = document.getElementById(id);
return element.selectionStart;
},
setIndexedDBItem: function (key, value) {
let idb = indexedDB.open("oqtane", 1);
idb.onupgradeneeded = function () {
let db = idb.result;
db.createObjectStore("items");
}
idb.onsuccess = function () {
let transaction = idb.result.transaction("items", "readwrite");
let collection = transaction.objectStore("items")
collection.put(value, key);
}
},
getIndexedDBItem: async function (key) {
let request = new Promise((resolve) => {
let idb = indexedDB.open("oqtane", 1);
idb.onupgradeneeded = function () {
let db = idb.result;
db.createObjectStore("items");
}
idb.onsuccess = function () {
let transaction = idb.result.transaction("items", "readonly");
let collection = transaction.objectStore("items");
let result = collection.get(key);
result.onsuccess = function (e) {
resolve(result.result);
}
}
});
let result = await request;
return result;
},
getIndexedDBKeys: async function () {
let request = new Promise((resolve) => {
let idb = indexedDB.open("oqtane", 1);
idb.onupgradeneeded = function () {
let db = idb.result;
db.createObjectStore("items");
}
idb.onsuccess = function () {
let transaction = idb.result.transaction("items", "readonly");
let collection = transaction.objectStore("items");
let result = collection.getAllKeys();
result.onsuccess = function (e) {
resolve(result.result);
}
}
});
let result = await request;
return result;
},
removeIndexedDBItem: function (key) {
let idb = indexedDB.open("oqtane", 1);
idb.onupgradeneeded = function () {
let db = idb.result;
db.createObjectStore("items");
}
idb.onsuccess = function () {
let transaction = idb.result.transaction("items", "readwrite");
let collection = transaction.objectStore("items");
collection.delete(key);
}
}
};