@rmdes/indiekit-endpoint-activitypub 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +6 -0
- package/lib/batch-refollow.js +23 -23
- package/lib/controllers/dashboard.js +0 -1
- package/lib/controllers/explore.js +3 -12
- package/lib/controllers/my-profile.js +5 -1
- package/lib/controllers/refollow.js +0 -2
- package/lib/federation-setup.js +10 -3
- package/lib/fedidb.js +17 -59
- package/lib/migrations/separate-mentions.js +7 -13
- package/lib/redis-cache.js +98 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
|
|
3
3
|
import { setupFederation, buildPersonActor } from "./lib/federation-setup.js";
|
|
4
|
+
import { initRedisCache } from "./lib/redis-cache.js";
|
|
4
5
|
import {
|
|
5
6
|
createFedifyMiddleware,
|
|
6
7
|
} from "./lib/federation-bridge.js";
|
|
@@ -1059,6 +1060,11 @@ export default class ActivityPubEndpoint {
|
|
|
1059
1060
|
console.warn("[ActivityPub] Profile seed failed:", error.message);
|
|
1060
1061
|
});
|
|
1061
1062
|
|
|
1063
|
+
// Initialize Redis cache for plugin-level KV (fedidb, batch-refollow, etc.)
|
|
1064
|
+
if (this.options.redisUrl) {
|
|
1065
|
+
initRedisCache(this.options.redisUrl);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1062
1068
|
// Set up Fedify Federation instance
|
|
1063
1069
|
const { federation } = setupFederation({
|
|
1064
1070
|
collections: this._collections,
|
package/lib/batch-refollow.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { Follow } from "@fedify/fedify/vocab";
|
|
15
15
|
import { logActivity } from "./activity-log.js";
|
|
16
|
+
import { cacheGet, cacheSet } from "./redis-cache.js";
|
|
16
17
|
|
|
17
18
|
const BATCH_SIZE = 10;
|
|
18
19
|
const DELAY_PER_FOLLOW = 3_000;
|
|
@@ -58,7 +59,7 @@ export async function startBatchRefollow(options) {
|
|
|
58
59
|
);
|
|
59
60
|
|
|
60
61
|
// Set job state to running
|
|
61
|
-
await setJobState(
|
|
62
|
+
await setJobState("running");
|
|
62
63
|
|
|
63
64
|
// Schedule first batch after startup delay
|
|
64
65
|
_timer = setTimeout(() => processNextBatch(options), STARTUP_DELAY);
|
|
@@ -81,7 +82,7 @@ export async function pauseBatchRefollow(collections) {
|
|
|
81
82
|
{ $set: { source: "import" } },
|
|
82
83
|
);
|
|
83
84
|
|
|
84
|
-
await setJobState(
|
|
85
|
+
await setJobState("paused");
|
|
85
86
|
console.info("[ActivityPub] Batch refollow: paused");
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -100,7 +101,7 @@ export async function resumeBatchRefollow(options) {
|
|
|
100
101
|
_timer = null;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
await setJobState(
|
|
104
|
+
await setJobState("running");
|
|
104
105
|
_timer = setTimeout(() => processNextBatch(options), DELAY_BETWEEN_BATCHES);
|
|
105
106
|
console.info("[ActivityPub] Batch refollow: resumed");
|
|
106
107
|
}
|
|
@@ -112,8 +113,8 @@ export async function resumeBatchRefollow(options) {
|
|
|
112
113
|
* @returns {Promise<object>} Status object
|
|
113
114
|
*/
|
|
114
115
|
export async function getBatchRefollowStatus(collections) {
|
|
115
|
-
const state = await
|
|
116
|
-
const status = state?.
|
|
116
|
+
const state = await cacheGet(KV_KEY);
|
|
117
|
+
const status = state?.status || "idle";
|
|
117
118
|
|
|
118
119
|
const [remaining, sent, failed, federated] = await Promise.all([
|
|
119
120
|
collections.ap_following.countDocuments({ source: "import" }),
|
|
@@ -138,8 +139,8 @@ export async function getBatchRefollowStatus(collections) {
|
|
|
138
139
|
federated,
|
|
139
140
|
completed,
|
|
140
141
|
progressPercent,
|
|
141
|
-
startedAt: state?.
|
|
142
|
-
updatedAt: state?.
|
|
142
|
+
startedAt: state?.startedAt || null,
|
|
143
|
+
updatedAt: state?.updatedAt || null,
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
146
|
|
|
@@ -152,8 +153,8 @@ async function processNextBatch(options) {
|
|
|
152
153
|
const { federation, collections, handle, publicationUrl } = options;
|
|
153
154
|
_timer = null;
|
|
154
155
|
|
|
155
|
-
const state = await
|
|
156
|
-
if (state?.
|
|
156
|
+
const state = await cacheGet(KV_KEY);
|
|
157
|
+
if (state?.status !== "running") return;
|
|
157
158
|
|
|
158
159
|
// Claim a batch atomically: set source to "refollow:pending"
|
|
159
160
|
const entries = [];
|
|
@@ -196,7 +197,7 @@ async function processNextBatch(options) {
|
|
|
196
197
|
);
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
await setJobState(
|
|
200
|
+
await setJobState("completed");
|
|
200
201
|
console.info("[ActivityPub] Batch refollow: completed");
|
|
201
202
|
return;
|
|
202
203
|
}
|
|
@@ -212,7 +213,7 @@ async function processNextBatch(options) {
|
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
// Update job state timestamp
|
|
215
|
-
await setJobState(
|
|
216
|
+
await setJobState("running");
|
|
216
217
|
|
|
217
218
|
// Schedule next batch
|
|
218
219
|
_timer = setTimeout(() => processNextBatch(options), DELAY_BETWEEN_BATCHES);
|
|
@@ -306,25 +307,24 @@ async function processOneFollow(options, entry) {
|
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
/**
|
|
309
|
-
* Set the batch re-follow job state in
|
|
310
|
+
* Set the batch re-follow job state in Redis.
|
|
310
311
|
*/
|
|
311
|
-
async function setJobState(
|
|
312
|
+
async function setJobState(status) {
|
|
312
313
|
const now = new Date().toISOString();
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
const existing = (await cacheGet(KV_KEY)) || {};
|
|
315
|
+
|
|
316
|
+
const newState = {
|
|
317
|
+
...existing,
|
|
318
|
+
status,
|
|
319
|
+
updatedAt: now,
|
|
319
320
|
};
|
|
320
321
|
|
|
321
322
|
// Only set startedAt on initial start or resume
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
update.$set["value.startedAt"] = now;
|
|
323
|
+
if (!existing.startedAt || (status === "running" && existing.status !== "running")) {
|
|
324
|
+
newState.startedAt = now;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
await
|
|
327
|
+
await cacheSet(KV_KEY, newState);
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
function sleep(ms) {
|
|
@@ -40,7 +40,6 @@ export function dashboardController(mountPath) {
|
|
|
40
40
|
// Get batch re-follow status for the progress section
|
|
41
41
|
const refollowStatus = await getBatchRefollowStatus({
|
|
42
42
|
ap_following: followingCollection,
|
|
43
|
-
ap_kv: application?.collections?.get("ap_kv"),
|
|
44
43
|
});
|
|
45
44
|
|
|
46
45
|
response.render("activitypub-dashboard", {
|
|
@@ -233,10 +233,7 @@ export function instanceSearchApiController(mountPath) {
|
|
|
233
233
|
return response.json([]);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
const
|
|
237
|
-
const kvCollection = application?.collections?.get("ap_kv") || null;
|
|
238
|
-
|
|
239
|
-
const results = await searchInstances(kvCollection, q, 8);
|
|
236
|
+
const results = await searchInstances(q, 8);
|
|
240
237
|
response.json(results);
|
|
241
238
|
} catch (error) {
|
|
242
239
|
next(error);
|
|
@@ -262,10 +259,7 @@ export function instanceCheckApiController(mountPath) {
|
|
|
262
259
|
return response.status(400).json({ supported: false, error: "Invalid domain" });
|
|
263
260
|
}
|
|
264
261
|
|
|
265
|
-
const
|
|
266
|
-
const kvCollection = application?.collections?.get("ap_kv") || null;
|
|
267
|
-
|
|
268
|
-
const result = await checkInstanceTimeline(kvCollection, validated);
|
|
262
|
+
const result = await checkInstanceTimeline(validated);
|
|
269
263
|
response.json(result);
|
|
270
264
|
} catch (error) {
|
|
271
265
|
next(error);
|
|
@@ -280,10 +274,7 @@ export function instanceCheckApiController(mountPath) {
|
|
|
280
274
|
export function popularAccountsApiController(mountPath) {
|
|
281
275
|
return async (request, response, next) => {
|
|
282
276
|
try {
|
|
283
|
-
const
|
|
284
|
-
const kvCollection = application?.collections?.get("ap_kv") || null;
|
|
285
|
-
|
|
286
|
-
const accounts = await getPopularAccounts(kvCollection, 50);
|
|
277
|
+
const accounts = await getPopularAccounts(50);
|
|
287
278
|
response.json(accounts);
|
|
288
279
|
} catch (error) {
|
|
289
280
|
next(error);
|
|
@@ -24,7 +24,6 @@ export function refollowPauseController(mountPath, plugin) {
|
|
|
24
24
|
const { application } = request.app.locals;
|
|
25
25
|
const collections = {
|
|
26
26
|
ap_following: application.collections.get("ap_following"),
|
|
27
|
-
ap_kv: application.collections.get("ap_kv"),
|
|
28
27
|
};
|
|
29
28
|
|
|
30
29
|
await pauseBatchRefollow(collections);
|
|
@@ -72,7 +71,6 @@ export function refollowStatusController(mountPath) {
|
|
|
72
71
|
const { application } = request.app.locals;
|
|
73
72
|
const collections = {
|
|
74
73
|
ap_following: application.collections.get("ap_following"),
|
|
75
|
-
ap_kv: application.collections.get("ap_kv"),
|
|
76
74
|
};
|
|
77
75
|
|
|
78
76
|
const status = await getBatchRefollowStatus(collections);
|
package/lib/federation-setup.js
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
Service,
|
|
35
35
|
} from "@fedify/fedify/vocab";
|
|
36
36
|
import { configure, getConsoleSink } from "@logtape/logtape";
|
|
37
|
-
import { RedisMessageQueue } from "@fedify/redis";
|
|
37
|
+
import { RedisMessageQueue, RedisKvStore } from "@fedify/redis";
|
|
38
38
|
import { createFederationDebugger } from "@fedify/debugger";
|
|
39
39
|
import Redis from "ioredis";
|
|
40
40
|
import { MongoKvStore } from "./kv-store.js";
|
|
@@ -100,6 +100,7 @@ export function setupFederation(options) {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
let queue;
|
|
103
|
+
let kv;
|
|
103
104
|
if (redisUrl) {
|
|
104
105
|
const redisQueue = new RedisMessageQueue(() => new Redis(redisUrl));
|
|
105
106
|
if (parallelWorkers > 1) {
|
|
@@ -111,15 +112,21 @@ export function setupFederation(options) {
|
|
|
111
112
|
queue = redisQueue;
|
|
112
113
|
console.info("[ActivityPub] Using Redis message queue (single worker)");
|
|
113
114
|
}
|
|
115
|
+
// Use Redis for Fedify KV store — idempotence records, public key cache,
|
|
116
|
+
// remote document cache. Redis handles TTL natively so entries auto-expire
|
|
117
|
+
// instead of growing unbounded in MongoDB.
|
|
118
|
+
kv = new RedisKvStore(new Redis(redisUrl));
|
|
119
|
+
console.info("[ActivityPub] Using Redis KV store for Fedify");
|
|
114
120
|
} else {
|
|
115
121
|
queue = new InProcessMessageQueue();
|
|
122
|
+
kv = new MongoKvStore(collections.ap_kv);
|
|
116
123
|
console.warn(
|
|
117
|
-
"[ActivityPub] Using in-process message queue (not recommended for production)",
|
|
124
|
+
"[ActivityPub] Using in-process message queue + MongoDB KV store (not recommended for production)",
|
|
118
125
|
);
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
const federation = createFederation({
|
|
122
|
-
kv
|
|
129
|
+
kv,
|
|
123
130
|
queue,
|
|
124
131
|
});
|
|
125
132
|
|
package/lib/fedidb.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FediDB API client with
|
|
2
|
+
* FediDB API client with Redis caching.
|
|
3
3
|
*
|
|
4
4
|
* Wraps https://api.fedidb.org/v1/ endpoints:
|
|
5
5
|
* - /servers — cursor-paginated list of known fediverse instances (ranked by size)
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* returns the same ranked list. We paginate through ~500 servers, cache the full
|
|
10
10
|
* corpus for 24 hours, and filter locally when the user searches.
|
|
11
11
|
*
|
|
12
|
-
* Cache TTL: 24 hours for both datasets.
|
|
12
|
+
* Cache TTL: 24 hours for both datasets (enforced by Redis TTL).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { cacheGet, cacheSet } from "./redis-cache.js";
|
|
16
|
+
|
|
15
17
|
const API_BASE = "https://api.fedidb.org/v1";
|
|
16
18
|
const FETCH_TIMEOUT_MS = 8_000;
|
|
17
|
-
const
|
|
19
|
+
const CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Fetch with timeout helper.
|
|
@@ -35,60 +37,21 @@ async function fetchWithTimeout(url) {
|
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
/**
|
|
39
|
-
* Get cached data from ap_kv, or null if expired/missing.
|
|
40
|
-
* @param {object} kvCollection - MongoDB ap_kv collection
|
|
41
|
-
* @param {string} cacheKey - Key to look up
|
|
42
|
-
* @returns {Promise<object|null>} Cached data or null
|
|
43
|
-
*/
|
|
44
|
-
async function getFromCache(kvCollection, cacheKey) {
|
|
45
|
-
if (!kvCollection) return null;
|
|
46
|
-
try {
|
|
47
|
-
const doc = await kvCollection.findOne({ _id: cacheKey });
|
|
48
|
-
if (!doc?.value?.data) return null;
|
|
49
|
-
const age = Date.now() - (doc.value.cachedAt || 0);
|
|
50
|
-
if (age > CACHE_TTL_MS) return null;
|
|
51
|
-
return doc.value.data;
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Write data to ap_kv cache.
|
|
59
|
-
* @param {object} kvCollection - MongoDB ap_kv collection
|
|
60
|
-
* @param {string} cacheKey - Key to store under
|
|
61
|
-
* @param {object} data - Data to cache
|
|
62
|
-
*/
|
|
63
|
-
async function writeToCache(kvCollection, cacheKey, data) {
|
|
64
|
-
if (!kvCollection) return;
|
|
65
|
-
try {
|
|
66
|
-
await kvCollection.updateOne(
|
|
67
|
-
{ _id: cacheKey },
|
|
68
|
-
{ $set: { value: { data, cachedAt: Date.now() } } },
|
|
69
|
-
{ upsert: true }
|
|
70
|
-
);
|
|
71
|
-
} catch {
|
|
72
|
-
// Cache write failure is non-critical
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
40
|
/**
|
|
77
41
|
* Fetch the FediDB server catalogue by paginating through cursor-based results.
|
|
78
42
|
* Cached for 24 hours as a single entry. The API ignores the `q` param and
|
|
79
43
|
* always returns a ranked list, so we collect a large corpus and filter locally.
|
|
80
44
|
*
|
|
81
45
|
* Paginates up to MAX_PAGES (13 pages × 40 = ~520 servers), which covers
|
|
82
|
-
* all well-known instances. Results are cached in
|
|
46
|
+
* all well-known instances. Results are cached in Redis for 24 hours.
|
|
83
47
|
*
|
|
84
|
-
* @param {object} kvCollection - MongoDB ap_kv collection
|
|
85
48
|
* @returns {Promise<Array>}
|
|
86
49
|
*/
|
|
87
50
|
const MAX_PAGES = 13;
|
|
88
51
|
|
|
89
|
-
async function getAllServers(
|
|
52
|
+
async function getAllServers() {
|
|
90
53
|
const cacheKey = "fedidb:servers-all";
|
|
91
|
-
const cached = await
|
|
54
|
+
const cached = await cacheGet(cacheKey);
|
|
92
55
|
if (cached) return cached;
|
|
93
56
|
|
|
94
57
|
const results = [];
|
|
@@ -123,7 +86,7 @@ async function getAllServers(kvCollection) {
|
|
|
123
86
|
}
|
|
124
87
|
|
|
125
88
|
if (results.length > 0) {
|
|
126
|
-
await
|
|
89
|
+
await cacheSet(cacheKey, results, CACHE_TTL_SECONDS);
|
|
127
90
|
}
|
|
128
91
|
} catch {
|
|
129
92
|
// Return whatever we collected so far
|
|
@@ -137,19 +100,16 @@ async function getAllServers(kvCollection) {
|
|
|
137
100
|
* Returns a flat array of { domain, software, description, mau, openRegistration }.
|
|
138
101
|
*
|
|
139
102
|
* Fetches the full server list once (cached 24h) and filters by domain/software match.
|
|
140
|
-
* FediDB's /v1/servers endpoint ignores the `q` param and always returns a static
|
|
141
|
-
* ranked list, so server-side filtering is the only way to get relevant results.
|
|
142
103
|
*
|
|
143
|
-
* @param {object} kvCollection - MongoDB ap_kv collection
|
|
144
104
|
* @param {string} query - Search term (e.g. "mast")
|
|
145
105
|
* @param {number} [limit=10] - Max results
|
|
146
106
|
* @returns {Promise<Array>}
|
|
147
107
|
*/
|
|
148
|
-
export async function searchInstances(
|
|
108
|
+
export async function searchInstances(query, limit = 10) {
|
|
149
109
|
const q = (query || "").trim().toLowerCase();
|
|
150
110
|
if (!q) return [];
|
|
151
111
|
|
|
152
|
-
const allServers = await getAllServers(
|
|
112
|
+
const allServers = await getAllServers();
|
|
153
113
|
|
|
154
114
|
return allServers
|
|
155
115
|
.filter(
|
|
@@ -166,13 +126,12 @@ export async function searchInstances(kvCollection, query, limit = 10) {
|
|
|
166
126
|
*
|
|
167
127
|
* Cached per domain for 24 hours.
|
|
168
128
|
*
|
|
169
|
-
* @param {object} kvCollection - MongoDB ap_kv collection
|
|
170
129
|
* @param {string} domain - Instance hostname
|
|
171
130
|
* @returns {Promise<{ supported: boolean, error: string|null }>}
|
|
172
131
|
*/
|
|
173
|
-
export async function checkInstanceTimeline(
|
|
132
|
+
export async function checkInstanceTimeline(domain) {
|
|
174
133
|
const cacheKey = `fedidb:timeline-check:${domain}`;
|
|
175
|
-
const cached = await
|
|
134
|
+
const cached = await cacheGet(cacheKey);
|
|
176
135
|
if (cached) return cached;
|
|
177
136
|
|
|
178
137
|
try {
|
|
@@ -193,7 +152,7 @@ export async function checkInstanceTimeline(kvCollection, domain) {
|
|
|
193
152
|
result = { supported: false, error: errorMsg };
|
|
194
153
|
}
|
|
195
154
|
|
|
196
|
-
await
|
|
155
|
+
await cacheSet(cacheKey, result, CACHE_TTL_SECONDS);
|
|
197
156
|
return result;
|
|
198
157
|
} catch {
|
|
199
158
|
return { supported: false, error: "Connection failed" };
|
|
@@ -206,13 +165,12 @@ export async function checkInstanceTimeline(kvCollection, domain) {
|
|
|
206
165
|
*
|
|
207
166
|
* Cached for 24 hours (single cache entry).
|
|
208
167
|
*
|
|
209
|
-
* @param {object} kvCollection - MongoDB ap_kv collection
|
|
210
168
|
* @param {number} [limit=50] - Max accounts to fetch
|
|
211
169
|
* @returns {Promise<Array>}
|
|
212
170
|
*/
|
|
213
|
-
export async function getPopularAccounts(
|
|
171
|
+
export async function getPopularAccounts(limit = 50) {
|
|
214
172
|
const cacheKey = `fedidb:popular-accounts:${limit}`;
|
|
215
|
-
const cached = await
|
|
173
|
+
const cached = await cacheGet(cacheKey);
|
|
216
174
|
if (cached) return cached;
|
|
217
175
|
|
|
218
176
|
try {
|
|
@@ -234,7 +192,7 @@ export async function getPopularAccounts(kvCollection, limit = 50) {
|
|
|
234
192
|
bio: (a.bio || "").replace(/<[^>]*>/g, "").slice(0, 120),
|
|
235
193
|
}));
|
|
236
194
|
|
|
237
|
-
await
|
|
195
|
+
await cacheSet(cacheKey, results, CACHE_TTL_SECONDS);
|
|
238
196
|
return results;
|
|
239
197
|
} catch {
|
|
240
198
|
return [];
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* New items will have URLs populated by the fixed extractObjectData() (Task 1).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { cacheGet, cacheSet } from "../redis-cache.js";
|
|
16
|
+
|
|
15
17
|
const MIGRATION_KEY = "migration:separate-mentions";
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -20,11 +22,11 @@ const MIGRATION_KEY = "migration:separate-mentions";
|
|
|
20
22
|
* @returns {Promise<{ skipped: boolean, updated: number }>}
|
|
21
23
|
*/
|
|
22
24
|
export async function runSeparateMentionsMigration(collections) {
|
|
23
|
-
const {
|
|
25
|
+
const { ap_timeline } = collections;
|
|
24
26
|
|
|
25
27
|
// Check if already completed
|
|
26
|
-
const state = await
|
|
27
|
-
if (state?.
|
|
28
|
+
const state = await cacheGet(MIGRATION_KEY);
|
|
29
|
+
if (state?.completed) {
|
|
28
30
|
return { skipped: true, updated: 0 };
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -35,11 +37,7 @@ export async function runSeparateMentionsMigration(collections) {
|
|
|
35
37
|
|
|
36
38
|
if (docs.length === 0) {
|
|
37
39
|
// No docs to migrate — mark complete immediately
|
|
38
|
-
await
|
|
39
|
-
{ _id: MIGRATION_KEY },
|
|
40
|
-
{ $set: { value: { completed: true, date: new Date().toISOString(), updated: 0 } } },
|
|
41
|
-
{ upsert: true }
|
|
42
|
-
);
|
|
40
|
+
await cacheSet(MIGRATION_KEY, { completed: true, date: new Date().toISOString(), updated: 0 });
|
|
43
41
|
return { skipped: false, updated: 0 };
|
|
44
42
|
}
|
|
45
43
|
|
|
@@ -78,11 +76,7 @@ export async function runSeparateMentionsMigration(collections) {
|
|
|
78
76
|
const updated = result.modifiedCount || 0;
|
|
79
77
|
|
|
80
78
|
// Mark migration complete
|
|
81
|
-
await
|
|
82
|
-
{ _id: MIGRATION_KEY },
|
|
83
|
-
{ $set: { value: { completed: true, date: new Date().toISOString(), updated } } },
|
|
84
|
-
{ upsert: true }
|
|
85
|
-
);
|
|
79
|
+
await cacheSet(MIGRATION_KEY, { completed: true, date: new Date().toISOString(), updated });
|
|
86
80
|
|
|
87
81
|
return { skipped: false, updated };
|
|
88
82
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-backed cache for plugin-level key-value storage.
|
|
3
|
+
*
|
|
4
|
+
* Replaces direct MongoDB ap_kv reads/writes for fedidb cache,
|
|
5
|
+
* batch-refollow state, and migration flags. Uses the same Redis
|
|
6
|
+
* connection as the Fedify message queue and KV store.
|
|
7
|
+
*
|
|
8
|
+
* All keys are prefixed with "indiekit:" to avoid collisions with
|
|
9
|
+
* Fedify's "fedify::" prefix.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import Redis from "ioredis";
|
|
13
|
+
|
|
14
|
+
const KEY_PREFIX = "indiekit:";
|
|
15
|
+
|
|
16
|
+
let _redis = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the Redis cache with a connection URL.
|
|
20
|
+
* Safe to call multiple times — reuses existing connection.
|
|
21
|
+
* @param {string} redisUrl - Redis connection URL
|
|
22
|
+
*/
|
|
23
|
+
export function initRedisCache(redisUrl) {
|
|
24
|
+
if (_redis) return;
|
|
25
|
+
if (!redisUrl) return;
|
|
26
|
+
_redis = new Redis(redisUrl);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the Redis client instance (for direct use if needed).
|
|
31
|
+
* @returns {import("ioredis").Redis|null}
|
|
32
|
+
*/
|
|
33
|
+
export function getRedisClient() {
|
|
34
|
+
return _redis;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get a value from Redis cache.
|
|
39
|
+
* @param {string} key
|
|
40
|
+
* @returns {Promise<unknown|null>}
|
|
41
|
+
*/
|
|
42
|
+
export async function cacheGet(key) {
|
|
43
|
+
if (!_redis) return null;
|
|
44
|
+
try {
|
|
45
|
+
const raw = await _redis.get(KEY_PREFIX + key);
|
|
46
|
+
if (raw === null) return null;
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Set a value in Redis cache with optional TTL.
|
|
55
|
+
* @param {string} key
|
|
56
|
+
* @param {unknown} value - Must be JSON-serializable
|
|
57
|
+
* @param {number} [ttlSeconds] - Optional TTL in seconds (0 = no expiry)
|
|
58
|
+
*/
|
|
59
|
+
export async function cacheSet(key, value, ttlSeconds = 0) {
|
|
60
|
+
if (!_redis) return;
|
|
61
|
+
try {
|
|
62
|
+
const raw = JSON.stringify(value);
|
|
63
|
+
if (ttlSeconds > 0) {
|
|
64
|
+
await _redis.set(KEY_PREFIX + key, raw, "EX", ttlSeconds);
|
|
65
|
+
} else {
|
|
66
|
+
await _redis.set(KEY_PREFIX + key, raw);
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// Cache write failure is non-critical
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete a key from Redis cache.
|
|
75
|
+
* @param {string} key
|
|
76
|
+
*/
|
|
77
|
+
export async function cacheDelete(key) {
|
|
78
|
+
if (!_redis) return;
|
|
79
|
+
try {
|
|
80
|
+
await _redis.del(KEY_PREFIX + key);
|
|
81
|
+
} catch {
|
|
82
|
+
// Ignore
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a key exists in Redis cache.
|
|
88
|
+
* @param {string} key
|
|
89
|
+
* @returns {Promise<boolean>}
|
|
90
|
+
*/
|
|
91
|
+
export async function cacheExists(key) {
|
|
92
|
+
if (!_redis) return false;
|
|
93
|
+
try {
|
|
94
|
+
return (await _redis.exists(KEY_PREFIX + key)) === 1;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|