source: Klonkt/src/services/Scheduler.js

main
Last change on this file was 1d1fdc9, checked in by Robin <roboburr@…>, 4 days ago

Tijdstempels: de posts-kant ook, en de tijdbom ontmanteld (shaer-a937)

Correctie op mijn eigen sluitreden bij 9946a68: ik schreef daar dat de
posts-sorteringen door isoSql lopen, en dat was niet zo -- ik had ze juist
overgeslagen omdat ze vandaag nog niet scheef staan.

Twee dingen alsnog:

De TIJDBOM uit de bead. Scheduler zette published_at op CURRENT_TIMESTAMP,
dus zodra iemand een post inplant en die vanzelf publiceert, staat er SQL-
notatie tussen de ISO's en sorteert het hele archief door elkaar. Die schrijft
nu ISO, net als de andere zes schrijfwegen.

En het VANGNET voor wat er al staat: de zeven posts-sorteringen (outbox,
backfill, featured tweemaal, muziek tweemaal, beheeroverzicht) lopen nu ook
door isoSql. Half normaliseren is de verwarrendste toestand -- dan hangt het
per scherm af of een gemengde kolom goed valt.

Onderweg gecontroleerd dat elke gebruiker van isoSql en NU_ISO ze ook
importeert: admin.js gebruikte hem zonder import, en dat was pas omgevallen
op het moment dat iemand het beheeroverzicht opende. Alle aangeraakte
bestanden door node --check.

Volle suite 1255 groen.

  • Property mode set to 100644
File size: 3.9 KB
Line 
1/**
2 * Scheduler — release planning (premium #3).
3 *
4 * Scheduled posts have status 'scheduled' + publish_at (future). A lightweight
5 * timer flips them to 'published' once publish_at is reached. This means public
6 * queries (status='published') need NO changes — a scheduled post simply isn't
7 * 'published' yet and therefore invisible until that moment.
8 */
9
10import db, { NU_ISO } from '../config/database.js';
11import HtmlSanitizerService from './HtmlSanitizerService.js';
12import ActivityPubService from './ActivityPubService.js';
13
14export function flipScheduledPosts() {
15 try {
16 const due = db.prepare(`
17 SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.cover_video_url, p.cover_alt, p.language, p.fan_only, p.nsfw, p.content_warning, p.poll_json,
18 p.published_at, p.publish_at, p.created_at, u.username
19 FROM posts p JOIN users u ON u.id = p.author_id
20 WHERE p.status = 'scheduled' AND p.publish_at IS NOT NULL AND datetime(p.publish_at) <= datetime('now')
21 `).all();
22 if (!due.length) return 0;
23 const upd = db.prepare(
24 `UPDATE posts SET status = 'published', published_at = COALESCE(published_at, publish_at, ${NU_ISO}) WHERE id = ?`
25 );
26 const ftsDel = db.prepare('DELETE FROM posts_fts WHERE post_id = ?');
27 const fts = db.prepare('INSERT INTO posts_fts(content, title, author, post_id) VALUES (?, ?, ?, ?)');
28 const siteStmt = db.prepare('SELECT * FROM sites WHERE id = ?');
29 for (const p of due) {
30 upd.run(p.id);
31 // Delete-before-insert so a re-scheduled (previously published) post doesn't
32 // get a duplicate FTS row → duplicate search hits.
33 try { ftsDel.run(p.id); fts.run(HtmlSanitizerService.toPlainText(p.content || ''), p.title || '', p.username || '', p.id); } catch { /* FTS failure is non-fatal */ }
34 // ActivityPub: federate the now-published post to followers (fan_only → followers-only).
35 try {
36 const site = siteStmt.get(p.site_id);
37 if (site) {
38 ActivityPubService.deliverCreate(site, {
39 id: p.id, slug: p.slug, title: p.title || p.slug,
40 content: p.content, cover_image_url: p.cover_image_url || null, cover_video_url: p.cover_video_url || null, cover_alt: p.cover_alt, language: p.language,
41 published_at: p.published_at || p.publish_at, created_at: p.created_at, fan_only: p.fan_only, nsfw: p.nsfw, content_warning: p.content_warning, poll_json: p.poll_json,
42 }).catch(() => { /* best-effort */ });
43 }
44 } catch { /* non-fatal */ }
45 }
46 return due.length;
47 } catch { return 0; }
48}
49
50// Close hosted polls whose endTime has passed: mark them closed (once) and push the final
51// tally + closed state to followers as Update(Question). The `closed` flag in poll_json
52// guards against re-sending — a poll is only processed on the tick that crosses its endTime.
53export function closeExpiredPolls() {
54 try {
55 const due = db.prepare(`
56 SELECT id, poll_json FROM posts
57 WHERE poll_json IS NOT NULL
58 AND status = 'published'
59 AND json_extract(poll_json, '$.endTime') IS NOT NULL
60 AND IFNULL(json_extract(poll_json, '$.closed'), 0) = 0
61 AND datetime(json_extract(poll_json, '$.endTime')) <= datetime('now')
62 `).all();
63 if (!due.length) return 0;
64 const upd = db.prepare('UPDATE posts SET poll_json = ? WHERE id = ?');
65 for (const p of due) {
66 let d; try { d = JSON.parse(p.poll_json); } catch { continue; }
67 d.closed = true;
68 upd.run(JSON.stringify(d), p.id);
69 ActivityPubService.deliverPollUpdate(p.id).catch(() => { /* best-effort */ });
70 }
71 return due.length;
72 } catch { return 0; }
73}
74
75let _timer = null;
76function tick() { flipScheduledPosts(); closeExpiredPolls(); }
77export function startScheduler() {
78 tick(); // run immediately on boot
79 if (_timer) return;
80 _timer = setInterval(tick, 60 * 1000); // every minute
81 if (_timer.unref) _timer.unref();
82}
Note: See TracBrowser for help on using the repository browser.