+32
-34
src/entrypoints/background.ts
+32
-34
src/entrypoints/background.ts
···
1
+
import { PersistentCache } from "@/lib/cache";
1
2
import { expect } from "@/lib/result";
2
3
import {
3
4
type Fronter,
···
7
8
getSpFronters,
8
9
memberUriString,
9
10
putFronter,
11
+
frontersCache,
10
12
} from "@/lib/utils";
11
13
import { parseResourceUri, ResourceUri } from "@atcute/lexicons";
12
14
···
15
17
main: () => {
16
18
console.log("setting up background script");
17
19
18
-
let fronters = new Map<ResourceUri, Fronter | null>();
19
-
const cacheFronter = (uri: ResourceUri, fronter: Fronter) => {
20
+
const cacheFronter = async (uri: ResourceUri, fronter: Fronter) => {
20
21
const parsedUri = expect(parseResourceUri(uri));
21
-
fronters.set(
22
+
await frontersCache.set(
22
23
`at://${fronter.did}/${parsedUri.collection!}/${parsedUri.rkey!}`,
23
24
fronter,
24
25
);
25
-
fronters.set(
26
+
await frontersCache.set(
26
27
`at://${fronter.handle}/${parsedUri.collection!}/${parsedUri.rkey!}`,
27
28
fronter,
28
29
);
···
72
73
for (const result of items) {
73
74
const resp = await putFronter(result.uri, members, authToken);
74
75
if (resp.ok) {
75
-
const parsedUri = cacheFronter(result.uri, resp.value);
76
+
const parsedUri = await cacheFronter(result.uri, resp.value);
76
77
results.push({
77
78
rkey: parsedUri.rkey!,
78
79
...resp.value,
···
100
101
sender: globalThis.Browser.runtime.MessageSender,
101
102
) => {
102
103
const handlePost = async (post: any) => {
103
-
const cachedFronter = fronters.get(post.uri);
104
+
const cachedFronter = await frontersCache.get(post.uri);
104
105
if (cachedFronter === null) return;
105
106
const promise = cachedFronter
106
107
? Promise.resolve(cachedFronter)
107
108
: getFronter(post.uri).then(async (fronter) => {
108
109
if (!fronter.ok) {
109
-
fronters.set(post.uri, null);
110
+
await frontersCache.set(post.uri, null);
110
111
return;
111
112
}
112
113
return fronter.value;
113
114
});
114
-
return promise.then((fronter) => {
115
+
return promise.then(async (fronter) => {
115
116
if (!fronter) return;
116
-
const parsedUri = cacheFronter(post.uri, fronter);
117
+
const parsedUri = await cacheFronter(post.uri, fronter);
117
118
return {
118
119
rkey: parsedUri.rkey!,
119
120
...fronter,
···
153
154
) => {
154
155
const data: any = JSON.parse(body);
155
156
const promises = (data.thread as any[]).flatMap((item) => {
156
-
const cachedFronter = fronters.get(item.uri);
157
-
if (cachedFronter === null) return [];
158
-
const promise = cachedFronter
159
-
? Promise.resolve(cachedFronter)
160
-
: getFronter(item.uri).then(async (fronter) => {
161
-
if (!fronter.ok) {
162
-
fronters.set(item.uri, null);
163
-
return;
164
-
}
165
-
return fronter.value;
166
-
});
167
-
return promise.then(async (fronter) => {
168
-
if (!fronter) return;
169
-
const parsedUri = cacheFronter(item.uri, fronter);
170
-
if (item.depth === 0) await setTabFronter(item.uri, fronter);
171
-
return {
172
-
rkey: parsedUri.rkey!,
173
-
displayName: item.value.post.author.displayName,
174
-
depth: item.depth,
175
-
...fronter,
176
-
};
157
+
return frontersCache.get(item.uri).then(async (cachedFronter) => {
158
+
if (cachedFronter === null) return [];
159
+
const promise = cachedFronter
160
+
? Promise.resolve(cachedFronter)
161
+
: getFronter(item.uri).then(async (fronter) => {
162
+
if (!fronter.ok) {
163
+
await frontersCache.set(item.uri, null);
164
+
return;
165
+
}
166
+
return fronter.value;
167
+
});
168
+
return promise.then(async (fronter) => {
169
+
if (!fronter) return;
170
+
const parsedUri = await cacheFronter(item.uri, fronter);
171
+
if (item.depth === 0) await setTabFronter(item.uri, fronter);
172
+
return {
173
+
rkey: parsedUri.rkey!,
174
+
displayName: item.value.post.author.displayName,
175
+
depth: item.depth,
176
+
...fronter,
177
+
};
178
+
});
177
179
});
178
180
});
179
181
const results = new Map(
···
224
226
await handleThread(message, sender);
225
227
break;
226
228
}
227
-
browser.tabs.sendMessage(sender.tab?.id!, {
228
-
type: "CACHED_FRONTERS",
229
-
fronters,
230
-
});
231
229
});
232
230
browser.runtime.onMessage.addListener(async (message, sender) => {
233
231
if (message.type !== "TAB_FRONTER") return;
+13
-2
src/entrypoints/content.ts
+13
-2
src/entrypoints/content.ts
···
1
+
import { decodeStorageKey } from "@/lib/cache";
1
2
import { expect } from "@/lib/result";
2
3
import {
3
4
Fronter,
···
120
121
const applyFrontersToPage = (fronters: Map<string, any>) => {
121
122
// console.log("applyFrontersToPage", fronters);
122
123
const match = parseSocialAppPostUrl(document.URL);
124
+
// console.log(match, fronters);
125
+
for (const el of document.querySelectorAll("[data-fronter]")) {
126
+
const previousFronter = el.getAttribute("data-fronter")!;
127
+
// remove fronter text
128
+
el.textContent = el.textContent.replace(` [f: ${previousFronter}]`, "");
129
+
el.removeAttribute("data-fronter");
130
+
}
131
+
if (fronters.size === 0) return;
123
132
for (const el of document.getElementsByTagName("a")) {
124
133
const path = `/${el.href.split("/").slice(3).join("/")}`;
125
134
const fronter = fronters.get(path);
···
139
148
};
140
149
let postTabObserver: MutationObserver | null = null;
141
150
window.addEventListener("message", (event) => {
142
-
if (event.data.type !== "CACHED_FRONTERS") return;
151
+
if (event.data.type !== "APPLY_CACHED_FRONTERS") return;
143
152
const applyFronters = () => {
144
153
const fronters = event.data.fronters as Map<string, Fronter | null>;
145
154
const updated = new Map(
146
-
fronters.entries().flatMap(([uri, fronter]) => {
155
+
fronters.entries().flatMap(([storageKey, fronter]) => {
147
156
if (!fronter) return [];
157
+
const uri = decodeStorageKey(storageKey);
148
158
const rkey = expect(parseResourceUri(uri)).rkey!;
149
159
return fronterGetSocialAppHrefs(fronter, rkey).map((href) => [
150
160
href,
···
152
162
]);
153
163
}),
154
164
);
165
+
// console.log("applying cached fronters");
155
166
applyFrontersToPage(updated);
156
167
};
157
168
// check if we are on profile so we can update fronters if the post tab is clicked on
+10
-17
src/entrypoints/isolated.content.ts
+10
-17
src/entrypoints/isolated.content.ts
···
1
-
import { Fronter, parseSocialAppPostUrl } from "@/lib/utils";
1
+
import { Fronter, frontersCache, parseSocialAppPostUrl } from "@/lib/utils";
2
2
import { ResourceUri } from "@atcute/lexicons";
3
3
4
4
export default defineContentScript({
···
6
6
runAt: "document_start",
7
7
world: "ISOLATED",
8
8
main: (ctx) => {
9
-
let fronters = new Map<ResourceUri, Fronter | null>();
10
-
11
-
const checkFronter = (url: string) => {
9
+
const checkFronter = async (url: string) => {
12
10
// match https://*/profile/<actor_identifier>/post/<rkey> regex with named params to extract actor_identifier and rkey
13
11
const match = parseSocialAppPostUrl(url);
14
12
if (!match) return false;
15
13
const recordUri =
16
14
`at://${match.actorIdentifier}/app.bsky.feed.post/${match.rkey}` as ResourceUri;
17
-
const fronter = fronters.get(recordUri);
15
+
const fronter = await frontersCache.get(recordUri);
18
16
if (!fronter) return false;
19
17
browser.runtime.sendMessage({
20
18
type: "TAB_FRONTER",
···
33
31
data,
34
32
});
35
33
});
36
-
const messageTypes = [
37
-
"TAB_FRONTER",
38
-
"THREAD_FRONTER",
39
-
"TIMELINE_FRONTER",
40
-
"CACHED_FRONTERS",
41
-
];
34
+
const messageTypes = ["TAB_FRONTER", "THREAD_FRONTER", "TIMELINE_FRONTER"];
42
35
browser.runtime.onMessage.addListener((message) => {
43
36
if (!messageTypes.includes(message.type)) return;
44
-
if (message.type === "CACHED_FRONTERS") {
45
-
fronters = message.fronters;
46
-
}
47
37
window.postMessage(message);
48
38
});
49
-
ctx.addEventListener(window, "wxt:locationchange", (event) => {
50
-
window.postMessage({ type: "CACHED_FRONTERS", fronters });
39
+
window.addEventListener("popstate", async (event) => {
40
+
window.postMessage({
41
+
type: "APPLY_CACHED_FRONTERS",
42
+
fronters: await frontersCache.getAll(),
43
+
});
51
44
// check for tab fronter for the current "post"
52
-
checkFronter(event.newUrl.toString());
45
+
await checkFronter(document.location.href);
53
46
});
54
47
55
48
// setup response "channel"
+110
src/lib/cache.ts
+110
src/lib/cache.ts
···
1
+
interface CachedItem<T> {
2
+
data: T;
3
+
timestamp: number;
4
+
}
5
+
6
+
export const decodeStorageKey = (storageKey: string) =>
7
+
atob(storageKey.split("_")[1]);
8
+
9
+
export class PersistentCache<T = any> {
10
+
private readonly keyPrefix: string;
11
+
private readonly expiryHours: number;
12
+
private readonly keysSetKey: `local:${string}`;
13
+
14
+
constructor(keyPrefix: string, expiryHours: number) {
15
+
this.keyPrefix = keyPrefix;
16
+
this.expiryHours = expiryHours;
17
+
this.keysSetKey = `local:${keyPrefix}_keys`;
18
+
}
19
+
20
+
private getCacheKey(key: string): `local:${string}` {
21
+
const safeKey = btoa(key);
22
+
return `local:${this.keyPrefix}_${safeKey}`;
23
+
}
24
+
25
+
private async getStoredKeys(): Promise<Set<string>> {
26
+
const keys = await storage.getItem<string[]>(this.keysSetKey);
27
+
return new Set(keys || []);
28
+
}
29
+
30
+
private async addKeyToSet(key: string): Promise<void> {
31
+
const keys = await this.getStoredKeys();
32
+
keys.add(key);
33
+
await storage.setItem(this.keysSetKey, Array.from(keys));
34
+
}
35
+
36
+
private async removeKeyFromSet(...key: string[]): Promise<void> {
37
+
const keys = await this.getStoredKeys();
38
+
for (const k of key) keys.delete(k);
39
+
await storage.setItem(this.keysSetKey, Array.from(keys));
40
+
}
41
+
42
+
async get(key: string): Promise<T | undefined> {
43
+
const cacheKey = this.getCacheKey(key);
44
+
const cached = await storage.getItem<CachedItem<T>>(cacheKey);
45
+
46
+
if (!cached) return undefined;
47
+
48
+
const now = Date.now();
49
+
const expiryTime = cached.timestamp + this.expiryHours * 60 * 60 * 1000;
50
+
51
+
if (this.expiryHours > 0 && now > expiryTime) {
52
+
await storage.removeItem(cacheKey);
53
+
return undefined;
54
+
}
55
+
56
+
return cached.data;
57
+
}
58
+
59
+
async set(key: string, value: T): Promise<void> {
60
+
const cacheKey = this.getCacheKey(key);
61
+
const cachedItem: CachedItem<T> = {
62
+
data: value,
63
+
timestamp: Date.now(),
64
+
};
65
+
await storage.setItem(cacheKey, cachedItem);
66
+
await this.addKeyToSet(key);
67
+
}
68
+
69
+
async remove(key: string): Promise<void> {
70
+
const cacheKey = this.getCacheKey(key);
71
+
await storage.removeItem(cacheKey);
72
+
await this.removeKeyFromSet(key);
73
+
}
74
+
75
+
async getAll(): Promise<Map<string, T>> {
76
+
const keys = await this.getStoredKeys();
77
+
78
+
if (keys.size === 0) {
79
+
return new Map();
80
+
}
81
+
82
+
const cacheKeys = Array.from(keys).map((key) => this.getCacheKey(key));
83
+
const items = await storage.getItems(cacheKeys);
84
+
85
+
const result = new Map<string, T>();
86
+
const now = Date.now();
87
+
const keysToRemove: string[] = [];
88
+
89
+
for (const { key, value } of items) {
90
+
const expiryTime = value.timestamp + this.expiryHours * 60 * 60 * 1000;
91
+
92
+
if (this.expiryHours > 0 && now > expiryTime) {
93
+
keysToRemove.push(key);
94
+
} else {
95
+
result.set(key, value.data);
96
+
}
97
+
}
98
+
99
+
// Clean up expired or missing items
100
+
if (keysToRemove.length > 0) {
101
+
const expiredCacheKeys = keysToRemove.map((key) => this.getCacheKey(key));
102
+
await Promise.all([
103
+
storage.removeItems(expiredCacheKeys),
104
+
this.removeKeyFromSet(...keysToRemove),
105
+
]);
106
+
}
107
+
108
+
return result;
109
+
}
110
+
}
+2
-2
src/lib/result.ts
+2
-2
src/lib/result.ts
+12
-4
src/lib/utils.ts
+12
-4
src/lib/utils.ts
···
25
25
WellKnownHandleResolver,
26
26
} from "@atcute/identity-resolver";
27
27
import { getAtprotoHandle, getPdsEndpoint } from "@atcute/identity";
28
+
import { PersistentCache } from "./cache";
28
29
29
30
export type Fronter = {
30
31
members: {
···
101
102
}
102
103
};
103
104
104
-
let memberCache = new Map<string, any>();
105
+
// Member cache instance
106
+
const memberCache = new PersistentCache("member_cache", 24);
107
+
105
108
export const fetchMember = async (
106
109
memberUri: MemberUri,
107
110
): Promise<string | undefined> => {
108
111
const s = memberUriString(memberUri);
109
-
const cached = memberCache.get(s);
112
+
const cached = await memberCache.get(s);
110
113
switch (memberUri.type) {
111
114
case "sp": {
112
115
if (cached) return cached.content.name;
···
122
125
);
123
126
if (!resp.ok) return;
124
127
const member = await resp.json();
125
-
memberCache.set(s, member);
128
+
await memberCache.set(s, member);
126
129
return member.content.name;
127
130
}
128
131
case "pk": {
···
132
135
);
133
136
if (!resp.ok) return;
134
137
const member = await resp.json();
135
-
memberCache.set(s, member);
138
+
await memberCache.set(s, member);
136
139
return member.name;
137
140
}
138
141
}
···
188
191
const handler = simpleFetchHandler({ service: pdsUrl });
189
192
return new AtpClient({ handler });
190
193
};
194
+
195
+
export const frontersCache = new PersistentCache<Fronter | null>(
196
+
"cachedFronters",
197
+
24,
198
+
);
191
199
192
200
export const getFronter = async <Uri extends ResourceUri>(
193
201
recordUri: Uri,