Skip to content

Commit 09ba146

Browse files
committed
refactor: messages cache
1 parent 30dbbd1 commit 09ba146

File tree

9 files changed

+437
-134
lines changed

9 files changed

+437
-134
lines changed

Diff for: app/src/js/core/messages.ts

+31-89
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,28 @@
11
/* global JsonWebKey */
2-
import { Message } from "../types";
3-
import type { Client } from "./client";
4-
import { Range } from "@quack/tools";
5-
6-
class MsgsRes<T> extends Range {
7-
data: T;
8-
fetched = new Date();
9-
10-
constructor(res: any) {
11-
super(res.from, res.to);
12-
this.data = res.data;
13-
}
14-
15-
static sort(repo: MsgsRes<any>[]) {
16-
return [...repo].sort((a, b) => a.from - b.from);
17-
}
18-
}
19-
20-
class MessagesCache<T> {
21-
repo: MsgsRes<T>[] = [];
22-
combineData: (r1: T, r2: T) => T;
23-
24-
constructor({ combine }: { combine: (r1: T, r2: T) => T }) {
25-
this.combineData = combine;
26-
document.addEventListener("freeze", () => {
27-
this.repo.length = 0;
28-
});
29-
}
30-
31-
update(entry: MsgsRes<T>) {
2+
import { Message, FullMessage } from "../types.ts";
3+
import type { Client } from "./client.ts";
4+
import { CacheEntry, Cache, mergeFn } from "@quack/tools";
5+
6+
class MsgsCacheEntry extends CacheEntry<FullMessage[]>{}
7+
8+
class MessagesCache extends Cache<FullMessage[]> {
9+
override merge = (a: CacheEntry<FullMessage[]>, b: CacheEntry<FullMessage[]>): CacheEntry<FullMessage[]> => (
10+
new CacheEntry(Math.min(a.from, b.from), Math.max(a.to, b.to), mergeFn(
11+
(_: FullMessage, b: FullMessage) => b,
12+
(a: FullMessage) => a.id.toString(),
13+
a.data,
14+
b.data
15+
)) as CacheEntry<FullMessage[]>
16+
)
17+
18+
19+
update(messages: FullMessage[]) {
20+
const dates = messages.map((m) => new Date(m.createdAt).getTime());
21+
const from = Math.min(...dates);
22+
const to = Math.max(...dates);
23+
const entry = new MsgsCacheEntry(from, to, messages);
3224
this.repo.push(entry);
3325
}
34-
35-
combine(r1: MsgsRes<T>, r2: MsgsRes<T>) {
36-
return new MsgsRes({
37-
...r2,
38-
...r1,
39-
from: Math.min(r1.from, r2.from),
40-
to: Math.max(r1.to, r2.to),
41-
data: this.combineData(r1.data, r2.data),
42-
});
43-
}
44-
45-
get(r: Range) {
46-
const relevant: MsgsRes<T>[] = MsgsRes.sort(this.repo).filter((r2) =>
47-
r.overlaps(r2)
48-
).reduce<any>((acc: MsgsRes<T>[], item: MsgsRes<T>) => {
49-
if (!acc[acc.length - 1]) {
50-
return [item];
51-
}
52-
const rest = acc.length > 1 ? acc.slice(0, acc.length - 1) : [];
53-
const last = acc[acc.length - 1];
54-
55-
if (item.overlaps(last)) {
56-
return [...rest, this.combine(last, item)];
57-
}
58-
acc.push(item);
59-
return acc;
60-
}, []);
61-
62-
const cache = relevant.find((rel: MsgsRes<T>) => r.containsEntirely(rel));
63-
if (cache) {
64-
return cache.data;
65-
}
66-
return null;
67-
}
6826
}
6927

7028
type MessageQuery = {
@@ -79,7 +37,7 @@ type MessageQuery = {
7937
};
8038

8139
export class MessageService {
82-
_cache: { [key: string]: MessagesCache<Message[]> };
40+
_cache: { [key: string]: MessagesCache };
8341
pending: { [key: string]: Promise<Message[]> } = {};
8442
dataContainer: (r1: Message[], r2: Message[]) => Message[];
8543
client: Client;
@@ -99,31 +57,21 @@ export class MessageService {
9957
if (pinned) key += "-pinned";
10058
if (search) key += `-search:${search}`;
10159
if (!this._cache[key]) {
102-
this._cache[key] = new MessagesCache({
103-
combine: this.dataContainer,
104-
});
60+
this._cache[key] = new MessagesCache();
10561
}
10662
return this._cache[key];
10763
}
10864

109-
getMaxDate(data: Message[]) {
110-
const dates = data.map((m) => new Date(m.createdAt).getTime());
111-
return Math.max(...dates);
112-
}
113-
114-
getMinDate(data: Message[]) {
115-
const dates = data.map((m) => new Date(m.createdAt).getTime());
116-
return Math.min(...dates);
117-
}
118-
11965
async _fetch(query: MessageQuery): Promise<Message[]> {
12066
const { channelId, parentId, before, after, limit, preprocess } = query;
121-
const to = before ? new Date(before).getTime() : Infinity;
122-
const from = after ? new Date(after).getTime() : -Infinity;
67+
const to = before ? new Date(before).getTime() : undefined;
68+
const from = after ? new Date(after).getTime() : undefined;
12369
if (to || from) {
124-
const cache = this.cache(query).get(new Range(from, to));
70+
console.log(from, to);
71+
const cache = this.cache(query).get({from, to});
12572
if (cache) {
126-
return cache.map((item) => ({ ...item })); //remove clonning
73+
console.log('using cache')
74+
return cache.data.map((item) => ({ ...item })); //remove clonning
12775
}
12876
}
12977
const data = await this.client.api.getMessages({
@@ -139,13 +87,7 @@ export class MessageService {
13987
const preprocessedData = preprocess ? await preprocess(data) : data;
14088

14189
if (data?.length > 0) {
142-
this.cache(query).update(
143-
new MsgsRes({
144-
from: after ? from : this.getMinDate(data),
145-
to: before ? to : this.getMaxDate(data),
146-
data: preprocessedData,
147-
}),
148-
);
90+
this.cache(query).update(preprocessedData);
14991
}
15092

15193
return preprocessedData;

Diff for: app/src/js/core/models/messages.ts

+21-4
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ export class MessagesModel {
4949
this.selected = selected ?? null;
5050
this.mode = selected ? "spotlight" : "live";
5151

52-
this._cleanups.push(client.on2("message", (msg) => this.onMessage(msg)));
52+
this._cleanups.push(client.on2("message", (msg: Message) => this.onMessage(msg)));
5353
this._cleanups.push(
54-
client.on2("message:remove", (msg) => this.onRemove(msg)),
54+
client.on2("message:remove", (msg: Message) => this.onRemove(msg)),
5555
);
5656
this._cleanups.push(this.subscribeUnfreeze());
5757
}
@@ -65,11 +65,17 @@ export class MessagesModel {
6565
console.log("focus");
6666
this.refresh();
6767
};
68+
const reconnect = () => {
69+
console.log("reconnect");
70+
this.refresh();
71+
};
6872
addEventListener("resume", resume);
6973
addEventListener("focus", focus);
74+
client.on("con:open", reconnect);
7075
return () => {
7176
removeEventListener("resume", resume);
7277
removeEventListener("focus", focus);
78+
client.off("con:open", reconnect);
7379
};
7480
};
7581

@@ -90,12 +96,23 @@ export class MessagesModel {
9096
const m = await this.decrypt(msg);
9197
runInAction(() => {
9298
this.ghosts = this.ghosts.filter((g) => g.clientId !== msg.clientId);
99+
if( msg.ephemeral ) {
100+
this.ghosts = mergeFn<MessageModel>(
101+
(a: MessageModel, b: MessageModel) => a.patch(b),
102+
({ id }: Message) => id,
103+
this.ghosts,
104+
m.map((m: FullMessage) => new MessageModel(m, this)),
105+
).sort((a: MessageModel, b: MessageModel) =>
106+
new Date(a.createdAt) < new Date(b.createdAt) ? 1 : -1
107+
);
108+
return;
109+
}
93110
this.list = mergeFn<MessageModel>(
94111
(a: MessageModel, b: MessageModel) => a.patch(b),
95-
({ id }) => id,
112+
({ id }: Message) => id,
96113
this.list,
97114
m.map((m: FullMessage) => new MessageModel(m, this)),
98-
).sort((a, b) =>
115+
).sort((a: MessageModel, b: MessageModel) =>
99116
new Date(a.createdAt) < new Date(b.createdAt) ? 1 : -1
100117
);
101118
});

Diff for: deno/tools/cache.test.ts

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { assertEquals } from "@std/assert";
2+
import { CacheEntry, Cache } from "./cache.ts";
3+
import { mergeRanges } from "./range.ts";
4+
5+
type Data = string[];
6+
7+
const merge = (a: CacheEntry<Data>, b: CacheEntry<Data>): CacheEntry<Data> => (
8+
new CacheEntry(Math.min(a.from, b.from), Math.max(a.to, b.to), [...a.data, ...b.data])
9+
)
10+
11+
const A = new CacheEntry(0, 1, ["A"]);
12+
const B = new CacheEntry(3, 6, ["B"]);
13+
const C = new CacheEntry(5, 10, ["C"]);
14+
const D = new CacheEntry(11, 12, ["D"]);
15+
const E = new CacheEntry(13, 16, ["D"]);
16+
E.timestamp = new Date().getTime() - 1000 * 60 * 60 * 2;
17+
18+
Deno.test("[CacheEntry] merge", () => {
19+
assertEquals(mergeRanges(merge, A, B, C, D).toString(), "[0, 1]: [A],[3, 10]: [B,C],[11, 12]: [D]");
20+
});
21+
22+
Deno.test("[Cache] query from - bottom", () => {
23+
const result = new Cache<Data>(A, B, C, D).get({ from: 3 });
24+
assertEquals(result?.toString(), "[3, 10]: [B,C]");
25+
});
26+
27+
Deno.test("[Cache] query from - top", () => {
28+
const result = new Cache<Data>(A, B, C, D).get({ from: 10 });
29+
assertEquals(result, null);
30+
});
31+
Deno.test("[Cache] query from - outside", () => {
32+
const result = new Cache<Data>(A, B, C, D).get({ from: 2 });
33+
assertEquals(result, null);
34+
});
35+
36+
Deno.test("[Cache] query from - inside", () => {
37+
const result = new Cache<Data>(A, B, C, D).get({ from: 4 });
38+
assertEquals(result?.toString(), "[3, 10]: [B,C]");
39+
});
40+
41+
Deno.test("[Cache] query to - bottom", () => {
42+
const result = new Cache<Data>(A, B, C, D).get({ to: 3 });
43+
assertEquals(result, null);
44+
});
45+
46+
Deno.test("[Cache] query to - top", () => {
47+
const result = new Cache<Data>(A, B, C, D).get({ to: 10 });
48+
assertEquals(result?.toString(), "[3, 10]: [B,C]");
49+
});
50+
Deno.test("[Cache] query to - outside", () => {
51+
const result = new Cache<Data>(A, B, C, D).get({ to: 2 });
52+
assertEquals(result, null);
53+
});
54+
55+
Deno.test("[Cache] query to - inside", () => {
56+
const result = new Cache<Data>(A, B, C, D).get({ to: 4 });
57+
assertEquals(result?.toString(), "[3, 10]: [B,C]");
58+
});
59+
60+
Deno.test("[Cache] query range - bottom", () => {
61+
const result = new Cache<Data>(A, B, C, D).get({ from: 3, to: 7 });
62+
assertEquals(result?.toString(), "[3, 10]: [B,C]");
63+
});
64+
65+
Deno.test("[Cache] query range - top", () => {
66+
const result = new Cache<Data>(A, B, C, D).get({ from: 4, to: 10 });
67+
assertEquals(result?.toString(), "[3, 10]: [B,C]");
68+
});
69+
70+
Deno.test("[Cache] query range - outside", () => {
71+
const result = new Cache<Data>(A, B, C, D).get({ from: 15, to: 20 });
72+
assertEquals(result, null);
73+
});
74+
75+
Deno.test("[Cache] query range - inside", () => {
76+
const result = new Cache<Data>(A, B, C, D).get({ from: 4, to: 6 });
77+
assertEquals(result?.toString(), "[3, 10]: [B,C]");
78+
});
79+
80+
Deno.test("[Cache] query range - holes", () => {
81+
const result = new Cache<Data>(A, B, C, D).get({ from: 0, to: 12 });
82+
assertEquals(result, null);
83+
});
84+
85+
Deno.test("[Cache] query range - holes", () => {
86+
const result = new Cache<Data>(A, B, C, D).get({ from: 4, to: 11 });
87+
assertEquals(result, null);
88+
});
89+
90+
Deno.test("[Cache] query range - holes", () => {
91+
const result = new Cache<Data>(A, B, C, D).get({ from: 1, to: 5 });
92+
assertEquals(result, null);
93+
});
94+
95+
Deno.test("[Cache] invalidate", () => {
96+
const cache = new Cache<Data>(A, B, C, D);
97+
cache.invalidate();
98+
assertEquals(cache.repo.length, 0);
99+
})
100+
101+
Deno.test("[Cache] cleanup", () => {
102+
const cache = new Cache<Data>(A, B, C, D, E);
103+
cache.cleanup();
104+
assertEquals(cache.get({ from: 3, to: 4 })?.toString(), "[3, 10]: [B,C]");
105+
assertEquals(cache.get({ from: 14, to: 15 }), null);
106+
});
107+
108+
Deno.test("[Cache] add entry", () => {
109+
const cache = new Cache<Data>(A, C, D);
110+
assertEquals(cache.get({ from: 3, to: 4 }), null);
111+
cache.addEntry(B);
112+
assertEquals(cache.get({ from: 3, to: 4 })?.toString(), "[3, 10]: [B,C]");
113+
});

Diff for: deno/tools/cache.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { mergeRanges, Range } from './range.ts';
2+
3+
export class CacheEntry<T extends any[]> extends Range {
4+
data: T;
5+
timestamp: number;
6+
7+
constructor(from: number, to: number, data: T) {
8+
super(from, to);
9+
this.data = data;
10+
this.timestamp = new Date().getTime();
11+
}
12+
13+
static sort(repo: CacheEntry<any>[]) {
14+
return [...repo].sort((a, b) => a.from - b.from);
15+
}
16+
17+
override toString() {
18+
return `${super.toString()}: [${this.data}]`;
19+
}
20+
}
21+
22+
export type CacheQuery = {
23+
from?: number;
24+
to?: number;
25+
}
26+
27+
export class Cache<T extends any[]> {
28+
repo: CacheEntry<T>[] = [];
29+
30+
merge = (a: CacheEntry<T>, b: CacheEntry<T>): CacheEntry<T> => (
31+
new CacheEntry(Math.min(a.from, b.from), Math.max(a.to, b.to), [...a.data, ...b.data]) as CacheEntry<T>
32+
)
33+
34+
constructor(...entries: CacheEntry<T>[]) {
35+
this.repo = entries;
36+
}
37+
38+
cleanup() {
39+
this.repo = this.repo.filter((entry) => entry.timestamp > new Date().getTime() - 1000 * 60 * 60);
40+
}
41+
42+
invalidate = () => {
43+
this.repo.length = 0;
44+
}
45+
46+
addEntry(entry: CacheEntry<T>) {
47+
this.repo.push(entry);
48+
this.repo = this.repo.sort((a, b) => a.from - b.from);
49+
}
50+
51+
get(q: CacheQuery): CacheEntry<T> | null{
52+
const repo = mergeRanges(this.merge, ...this.repo);
53+
if("from" in q && "to" in q && q.from !== undefined && q.to !== undefined){
54+
const {from, to} = q;
55+
return repo.find((entry) => entry.containsEntirely(new Range(from, to))) || null;
56+
}
57+
if("from" in q && q.from !== undefined){
58+
const { from } = q;
59+
return repo.find((entry) => entry.containsPointFrom(from)) || null;
60+
}
61+
if("to" in q && q.to !== undefined){
62+
const { to } = q;
63+
return repo.find((entry) => entry.containsPointTo(to)) || null;
64+
}
65+
return null;
66+
}
67+
}

0 commit comments

Comments
 (0)