initial commit
This commit is contained in:
252
unity/Assets/Guildlib/Runtime/ShardClient.cs
Normal file
252
unity/Assets/Guildlib/Runtime/ShardClient.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Guildlib.Runtime
|
||||
{
|
||||
// ── Wire types ────────────────────────────────────────────────────────────
|
||||
|
||||
public class ManifestRow
|
||||
{
|
||||
public string id;
|
||||
public long version;
|
||||
public string checksum;
|
||||
}
|
||||
|
||||
public class ShardManifest
|
||||
{
|
||||
public string shard;
|
||||
public List<ManifestRow> rows;
|
||||
}
|
||||
|
||||
public class PullRequest
|
||||
{
|
||||
public string shard;
|
||||
public List<string> ids;
|
||||
|
||||
// Optional platform filter — only pull rows tagged for these platforms.
|
||||
// The server will include rows tagged "all" regardless of this list.
|
||||
// Set to null to pull everything.
|
||||
public List<string> filterPlatforms;
|
||||
|
||||
// Optional tag filter — only pull rows that have at least one of these tags.
|
||||
// Set to null to pull everything.
|
||||
public List<string> filterTags;
|
||||
}
|
||||
|
||||
public class PullResponse
|
||||
{
|
||||
public string shard;
|
||||
public List<Dictionary<string,object>> rows;
|
||||
}
|
||||
|
||||
public class PushPayload
|
||||
{
|
||||
public string shard;
|
||||
public string typeName;
|
||||
public Dictionary<string,object> data;
|
||||
}
|
||||
|
||||
public class SyncResult
|
||||
{
|
||||
public string shard;
|
||||
public int added;
|
||||
public int updated;
|
||||
public int skipped;
|
||||
}
|
||||
|
||||
// ── ShardClient ───────────────────────────────────────────────────────────
|
||||
|
||||
public class ShardClient
|
||||
{
|
||||
readonly GuildlibConfig config;
|
||||
readonly HttpClient http;
|
||||
readonly string shardName;
|
||||
readonly string dbPath;
|
||||
|
||||
// Local manifest: entryId -> (version, checksum)
|
||||
Dictionary<string,(long version, string checksum)> localManifest = new();
|
||||
|
||||
public ShardClient(GuildlibConfig cfg, string shard)
|
||||
{
|
||||
config = cfg;
|
||||
shardName = shard;
|
||||
http = new HttpClient();
|
||||
if (!string.IsNullOrEmpty(cfg.apiKey))
|
||||
http.DefaultRequestHeaders.Add("X-Api-Key", cfg.apiKey);
|
||||
|
||||
var dir = Path.Combine(Application.persistentDataPath, cfg.shardFolder);
|
||||
Directory.CreateDirectory(dir);
|
||||
dbPath = Path.Combine(dir, $"{shard}.sqlite");
|
||||
|
||||
LoadLocalManifest();
|
||||
}
|
||||
|
||||
// ── Pull ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Pull all changed rows from the server for this shard.
|
||||
///
|
||||
/// platformFilter: if non-null, only pull rows whose platforms include
|
||||
/// at least one of the given values, or "all".
|
||||
/// Example: new List<string>{"pc","ps5"}
|
||||
///
|
||||
/// tagFilter: if non-null, only pull rows with at least one matching tag.
|
||||
/// Example: new List<string>{"release","base_game"}
|
||||
///
|
||||
/// Both filters are applied server-side so no extra data crosses the wire.
|
||||
/// </summary>
|
||||
public async Task<SyncResult> PullAsync(
|
||||
List<string> platformFilter = null,
|
||||
List<string> tagFilter = null)
|
||||
{
|
||||
// 1 — Fetch server manifest (lightweight: id/version/checksum only)
|
||||
var manifestJson = await http.GetStringAsync($"{config.serverUrl}/manifest/{shardName}");
|
||||
var serverManifest = JsonConvert.DeserializeObject<ShardManifest>(manifestJson);
|
||||
|
||||
// 2 — Diff against local manifest
|
||||
var needIds = new List<string>();
|
||||
foreach (var row in serverManifest.rows)
|
||||
{
|
||||
if (!localManifest.TryGetValue(row.id, out var local) ||
|
||||
local.version < row.version ||
|
||||
local.checksum != row.checksum)
|
||||
needIds.Add(row.id);
|
||||
}
|
||||
|
||||
var result = new SyncResult
|
||||
{
|
||||
shard = shardName,
|
||||
skipped = serverManifest.rows.Count - needIds.Count
|
||||
};
|
||||
if (needIds.Count == 0) return result;
|
||||
|
||||
// 3 — Request only the delta rows, with optional server-side filtering
|
||||
var pullReq = new PullRequest
|
||||
{
|
||||
shard = shardName,
|
||||
ids = needIds,
|
||||
filterPlatforms = platformFilter,
|
||||
filterTags = tagFilter,
|
||||
};
|
||||
var reqJson = JsonConvert.SerializeObject(pullReq);
|
||||
var response = await http.PostAsync(
|
||||
$"{config.serverUrl}/pull",
|
||||
new StringContent(reqJson, Encoding.UTF8, "application/json"));
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var pullJson = await response.Content.ReadAsStringAsync();
|
||||
var pullResp = JsonConvert.DeserializeObject<PullResponse>(pullJson);
|
||||
|
||||
// 4 — Upsert into local SQLite shard
|
||||
int added = 0, updated = 0;
|
||||
using var db = new LocalShardDb(dbPath);
|
||||
db.EnsureTable(shardName);
|
||||
foreach (var row in pullResp.rows)
|
||||
{
|
||||
bool isNew = !localManifest.ContainsKey(row["id"].ToString());
|
||||
db.Upsert(shardName, row);
|
||||
if (isNew) added++; else updated++;
|
||||
}
|
||||
|
||||
// 5 — Persist updated manifest
|
||||
foreach (var mr in serverManifest.rows)
|
||||
localManifest[mr.id] = (mr.version, mr.checksum);
|
||||
SaveLocalManifest();
|
||||
|
||||
result.added = added;
|
||||
result.updated = updated;
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Push ─────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task PushAsync(DataEntry entry)
|
||||
{
|
||||
var data = EntryToDict(entry);
|
||||
var payload = new PushPayload
|
||||
{
|
||||
shard = shardName,
|
||||
typeName = entry.typeName,
|
||||
data = data,
|
||||
};
|
||||
var json = JsonConvert.SerializeObject(payload);
|
||||
var response = await http.PostAsync(
|
||||
$"{config.serverUrl}/push",
|
||||
new StringContent(json, Encoding.UTF8, "application/json"));
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// ── Local manifest ────────────────────────────────────────────────────
|
||||
|
||||
void LoadLocalManifest()
|
||||
{
|
||||
var path = dbPath + ".manifest.json";
|
||||
if (!File.Exists(path)) return;
|
||||
try
|
||||
{
|
||||
var raw = File.ReadAllText(path);
|
||||
localManifest = JsonConvert.DeserializeObject<
|
||||
Dictionary<string,(long, string)>>(raw) ?? new();
|
||||
}
|
||||
catch { localManifest = new(); }
|
||||
}
|
||||
|
||||
void SaveLocalManifest()
|
||||
{
|
||||
File.WriteAllText(dbPath + ".manifest.json",
|
||||
JsonConvert.SerializeObject(localManifest, Formatting.Indented));
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
static Dictionary<string,object> EntryToDict(DataEntry entry)
|
||||
{
|
||||
var dict = new Dictionary<string,object>();
|
||||
foreach (var f in entry.GetType().GetFields(
|
||||
System.Reflection.BindingFlags.Public |
|
||||
System.Reflection.BindingFlags.Instance))
|
||||
{
|
||||
dict[f.Name] = f.GetValue(entry);
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
|
||||
// ── LocalShardDb stub ─────────────────────────────────────────────────────
|
||||
// Replace the method bodies with your chosen Unity SQLite package.
|
||||
// Recommended: https://github.com/gkoreman/SQLite4Unity3d
|
||||
// or sqlite-net-pcl via NuGet
|
||||
|
||||
public class LocalShardDb : IDisposable
|
||||
{
|
||||
readonly string path;
|
||||
public LocalShardDb(string dbPath) { path = dbPath; }
|
||||
|
||||
public void EnsureTable(string tableName)
|
||||
{
|
||||
// CREATE TABLE IF NOT EXISTS <tableName> (
|
||||
// id TEXT PRIMARY KEY,
|
||||
// type_name TEXT,
|
||||
// parent_id TEXT,
|
||||
// platforms TEXT, -- JSON array
|
||||
// tags TEXT, -- JSON array
|
||||
// row_version INTEGER,
|
||||
// data_json TEXT,
|
||||
// updated_at INTEGER
|
||||
// )
|
||||
}
|
||||
|
||||
public void Upsert(string tableName, Dictionary<string,object> row)
|
||||
{
|
||||
// INSERT OR REPLACE INTO <tableName> VALUES (...)
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user