@rmdes/indiekit-endpoint-rss 1.0.16 → 1.0.17
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 +31 -17
- package/lib/controllers/feeds.js +1 -1
- package/lib/sync.js +46 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -6,14 +6,11 @@ import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
|
6
6
|
import { feedsController } from "./lib/controllers/feeds.js";
|
|
7
7
|
import { itemsController } from "./lib/controllers/items.js";
|
|
8
8
|
import { statusController } from "./lib/controllers/status.js";
|
|
9
|
-
import { startSync } from "./lib/sync.js";
|
|
9
|
+
import { startSync, stopSync } from "./lib/sync.js";
|
|
10
10
|
import { waitForReady } from "@rmdes/indiekit-startup-gate";
|
|
11
11
|
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
|
|
14
|
-
const protectedRouter = express.Router();
|
|
15
|
-
const publicRouter = express.Router();
|
|
16
|
-
|
|
17
14
|
const defaults = {
|
|
18
15
|
mountPath: "/rssapi",
|
|
19
16
|
syncInterval: 900_000, // 15 minutes
|
|
@@ -56,44 +53,60 @@ export default class RssEndpoint {
|
|
|
56
53
|
/**
|
|
57
54
|
* Protected routes (require authentication)
|
|
58
55
|
* Admin dashboard and feed management (write operations)
|
|
56
|
+
*
|
|
57
|
+
* Built once per instance. Indiekit reads this getter twice while mounting
|
|
58
|
+
* (once to test it, once to pass it to router.use), so a router built on
|
|
59
|
+
* every read registers every handler twice.
|
|
60
|
+
* @returns {express.Router}
|
|
59
61
|
*/
|
|
60
62
|
get routes() {
|
|
63
|
+
if (this._routes) return this._routes;
|
|
64
|
+
|
|
65
|
+
const router = express.Router();
|
|
66
|
+
|
|
61
67
|
// Dashboard
|
|
62
|
-
|
|
68
|
+
router.get("/", dashboardController.get);
|
|
63
69
|
|
|
64
70
|
// Manual sync trigger
|
|
65
|
-
|
|
71
|
+
router.post("/sync", dashboardController.sync);
|
|
66
72
|
|
|
67
73
|
// Clear items and re-sync
|
|
68
|
-
|
|
74
|
+
router.post("/clear-resync", dashboardController.clearResync);
|
|
69
75
|
|
|
70
76
|
// Feed management (protected - requires auth)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
router.post("/api/feeds", express.json(), feedsController.add);
|
|
78
|
+
router.delete("/api/feeds/:id", feedsController.remove);
|
|
79
|
+
router.patch("/api/feeds/:id", express.json(), feedsController.toggle);
|
|
74
80
|
|
|
75
81
|
// Manual refresh (protected)
|
|
76
|
-
|
|
82
|
+
router.post("/api/refresh", statusController.refresh);
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
this._routes = router;
|
|
85
|
+
return router;
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
/**
|
|
82
89
|
* Public routes (no authentication required)
|
|
83
90
|
* Read-only JSON API endpoints for frontend
|
|
91
|
+
* @returns {express.Router}
|
|
84
92
|
*/
|
|
85
93
|
get routesPublic() {
|
|
94
|
+
if (this._routesPublic) return this._routesPublic;
|
|
95
|
+
|
|
96
|
+
const router = express.Router();
|
|
97
|
+
|
|
86
98
|
// Feeds API (read-only)
|
|
87
|
-
|
|
99
|
+
router.get("/api/feeds", feedsController.list);
|
|
88
100
|
|
|
89
101
|
// Items API (read-only)
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
router.get("/api/items", itemsController.list);
|
|
103
|
+
router.get("/api/items/:id", itemsController.get);
|
|
92
104
|
|
|
93
105
|
// Status API (read-only)
|
|
94
|
-
|
|
106
|
+
router.get("/api/status", statusController.status);
|
|
95
107
|
|
|
96
|
-
|
|
108
|
+
this._routesPublic = router;
|
|
109
|
+
return router;
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
init(Indiekit) {
|
|
@@ -121,5 +134,6 @@ export default class RssEndpoint {
|
|
|
121
134
|
|
|
122
135
|
destroy() {
|
|
123
136
|
this._stopGate?.();
|
|
137
|
+
stopSync();
|
|
124
138
|
}
|
|
125
139
|
}
|
package/lib/controllers/feeds.js
CHANGED
package/lib/sync.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RssClient } from "./rss-client.js";
|
|
2
2
|
|
|
3
3
|
let syncInterval = null;
|
|
4
|
+
let initialSyncTimeout = null;
|
|
4
5
|
let syncState = {
|
|
5
6
|
lastSync: null,
|
|
6
7
|
syncing: false,
|
|
@@ -25,12 +26,16 @@ export function getSyncState() {
|
|
|
25
26
|
export function startSync(Indiekit, options) {
|
|
26
27
|
const intervalMs = options.syncInterval || 900_000; // 15 minutes default
|
|
27
28
|
|
|
29
|
+
// Starting twice would orphan the previous timers with no handle to clear.
|
|
30
|
+
stopSync();
|
|
31
|
+
|
|
28
32
|
console.log(
|
|
29
33
|
`[RSS] Starting background sync with ${intervalMs / 60_000}min interval`
|
|
30
34
|
);
|
|
31
35
|
|
|
32
36
|
// Initial sync after delay
|
|
33
|
-
setTimeout(() => {
|
|
37
|
+
initialSyncTimeout = setTimeout(() => {
|
|
38
|
+
initialSyncTimeout = null;
|
|
34
39
|
runSync(Indiekit, options).catch((err) => {
|
|
35
40
|
console.error("[RSS] Initial sync error:", err.message);
|
|
36
41
|
});
|
|
@@ -48,9 +53,16 @@ export function startSync(Indiekit, options) {
|
|
|
48
53
|
* Stop background sync
|
|
49
54
|
*/
|
|
50
55
|
export function stopSync() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
const wasRunning = Boolean(syncInterval || initialSyncTimeout);
|
|
57
|
+
|
|
58
|
+
// Both timers matter: destroy() within the first 10 seconds would otherwise
|
|
59
|
+
// still fire an initial sync against a torn-down plugin.
|
|
60
|
+
clearTimeout(initialSyncTimeout);
|
|
61
|
+
initialSyncTimeout = null;
|
|
62
|
+
clearInterval(syncInterval);
|
|
63
|
+
syncInterval = null;
|
|
64
|
+
|
|
65
|
+
if (wasRunning) {
|
|
54
66
|
console.log("[RSS] Background sync stopped");
|
|
55
67
|
}
|
|
56
68
|
}
|
|
@@ -88,6 +100,7 @@ export async function runSync(dbOrIndiekit, options) {
|
|
|
88
100
|
|
|
89
101
|
// Create indexes if they don't exist
|
|
90
102
|
await createIndexes(feedsCollection, itemsCollection);
|
|
103
|
+
await normalizeLegacyFeedDates(feedsCollection);
|
|
91
104
|
|
|
92
105
|
// Get all enabled feeds
|
|
93
106
|
const feeds = await feedsCollection.find({ enabled: true }).toArray();
|
|
@@ -258,6 +271,35 @@ async function createIndexes(feedsCollection, itemsCollection) {
|
|
|
258
271
|
await itemsCollection.createIndex({ fetchedAt: -1 });
|
|
259
272
|
}
|
|
260
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Rewrite legacy BSON Date values of `addedAt` as ISO 8601 strings.
|
|
276
|
+
*
|
|
277
|
+
* Indiekit stores dates as ISO strings. Feeds added before that was fixed hold
|
|
278
|
+
* a BSON Date, and in BSON a Date sorts after every string — so a mixed
|
|
279
|
+
* collection buries newly added feeds at the bottom of { addedAt: -1 }.
|
|
280
|
+
* No-op once every row has been converted.
|
|
281
|
+
* @param {Collection} feedsCollection - Feeds collection
|
|
282
|
+
* @returns {Promise<number>} Number of feeds normalized
|
|
283
|
+
*/
|
|
284
|
+
export async function normalizeLegacyFeedDates(feedsCollection) {
|
|
285
|
+
const legacy = await feedsCollection
|
|
286
|
+
.find({ addedAt: { $type: "date" } })
|
|
287
|
+
.toArray();
|
|
288
|
+
|
|
289
|
+
for (const feed of legacy) {
|
|
290
|
+
await feedsCollection.updateOne(
|
|
291
|
+
{ _id: feed._id },
|
|
292
|
+
{ $set: { addedAt: feed.addedAt.toISOString() } },
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (legacy.length > 0) {
|
|
297
|
+
console.log(`[RSS] Normalized addedAt on ${legacy.length} feed(s)`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return legacy.length;
|
|
301
|
+
}
|
|
302
|
+
|
|
261
303
|
/**
|
|
262
304
|
* Prune items older than the retention period.
|
|
263
305
|
*
|
package/package.json
CHANGED