1 module discord.w.cache;
2 
3 import std.algorithm;
4 import std.typecons;
5 import std.traits;
6 
7 package alias Identity(alias A) = A;
8 
9 // TODO: implement redis or write to disk
10 
11 class SimpleCache(T, string idMember = "id", size_t limit = size_t.max)
12 {
13 @safe:
14 	alias IDType = typeof(__traits(getMember, T.init, idMember));
15 
16 	private bool locked;
17 	T[] entries;
18 	size_t index;
19 
20 	private void putUnsafe(T data)
21 	{
22 		index = entries.length;
23 		static if (limit != size_t.max)
24 		{
25 			if (index >= limit)
26 				index = (index % limit);
27 			else
28 				entries.length++;
29 		}
30 		else
31 			entries.length++;
32 		entries[index] = data;
33 	}
34 
35 	void put(T data)
36 	{
37 		if (locked)
38 			assert(false, "Attempted to update cache while locked (deadlock)");
39 		synchronized (this)
40 		{
41 			auto id = __traits(getMember, data, idMember);
42 			if (entries.canFind!((a) => __traits(getMember, a, idMember) == id))
43 				throw new Exception("Entry with this ID already in cache");
44 			putUnsafe(data);
45 		}
46 	}
47 
48 	bool has(IDType id)
49 	{
50 		if (locked)
51 			assert(false, "Attempted to update cache while locked (deadlock)");
52 		synchronized (this)
53 		{
54 			return entries.canFind!(a => __traits(getMember, a, idMember) == id);
55 		}
56 	}
57 
58 	/// Returns: the IDs which could not been found
59 	IDType[] removeAll(IDType[] ids)
60 	{
61 		if (locked)
62 			assert(false, "Attempted to update cache while locked (deadlock)");
63 		synchronized (this)
64 		{
65 			foreach_reverse (i, element; entries)
66 			{
67 				auto rm = ids.countUntil(__traits(getMember, element, idMember));
68 				if (rm != -1)
69 				{
70 					ids = ids.remove!(SwapStrategy.unstable)(rm);
71 					entries = entries.remove(i);
72 				}
73 			}
74 			return ids;
75 		}
76 	}
77 
78 	bool remove(IDType id)
79 	{
80 		if (locked)
81 			assert(false, "Attempted to update cache while locked (deadlock)");
82 		synchronized (this)
83 		{
84 			auto index = entries.countUntil!(a => __traits(getMember, a, idMember) == id);
85 			if (index == -1)
86 				return false;
87 			entries = entries.remove(index);
88 			return true;
89 		}
90 	}
91 
92 	alias get = update;
93 
94 	T update(IDType id, scope void delegate(scope ref T) @safe updater = null, bool put = false)
95 	{
96 		if (locked)
97 			assert(false, "Attempted to update cache while locked (deadlock)");
98 		synchronized (this)
99 		{
100 			auto index = entries.countUntil!(a => __traits(getMember, a, idMember) == id);
101 			if (index == -1)
102 			{
103 				if (put)
104 				{
105 					T data;
106 					__traits(getMember, data, idMember) = id;
107 					updater(data);
108 					putUnsafe(data);
109 					return data;
110 				}
111 				else
112 				{
113 					debug (DumpCache)
114 					{
115 						import vibe.core.path : NativePath;
116 						import vibe.core.file : writeFileUTF8;
117 						import vibe.data.json : serializeToJsonString;
118 						import vibe.core.log : logDebugV;
119 
120 						writeFileUTF8(NativePath("debug-" ~ T.stringof ~ "-cache.json"),
121 								serializeToJsonString(entries));
122 						logDebugV("Dumped cache to debug-" ~ T.stringof ~ "-cache.json");
123 					}
124 				else
125 					{
126 						import vibe.core.log : logDebugV;
127 
128 						logDebugV("Cache entries: %(%s, %)", entries.map!("a." ~ idMember));
129 					}
130 					throw new Exception("Tried to update non existant cache entry");
131 				}
132 			}
133 			if (updater)
134 			{
135 				locked = true;
136 				updater(entries[index]);
137 				locked = false;
138 			}
139 			return entries[index];
140 		}
141 	}
142 
143 	T patch(T value, bool put = false)
144 	{
145 		return update(__traits(getMember, value, idMember), (scope ref v) {
146 			foreach (name; FieldNameTuple!T)
147 			{
148 				static if (is(typeof(__traits(getMember, value, name)) : Nullable!U, U))
149 				{
150 					if (!__traits(getMember, value, name).isNull)
151 						__traits(getMember, v, name) = __traits(getMember, value, name);
152 				}
153 				else static if (isArray!(typeof(__traits(getMember, value, name))))
154 				{
155 					if (__traits(getMember, value, name).length)
156 						__traits(getMember, v, name) = __traits(getMember, value, name);
157 				}
158 				else static if (__traits(compiles, __traits(getMember, value, name) == null))
159 				{
160 					if (__traits(getMember, value, name) != null)
161 						__traits(getMember, v, name) = __traits(getMember, value, name);
162 				}
163 				else
164 					__traits(getMember, v, name) = __traits(getMember, value, name);
165 			}
166 		}, put);
167 	}
168 }