source: Klonkt/src/services/SqliteSessionStore.js@ 9946a68

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

Tijdstempels, bronkant: een spelling bij het schrijven, plus migratie (shaer-a937)

De leeskant van 8151340 is het vangnet; dit haalt de oorzaak weg. Zolang
er twee vormen binnenkomen blijft elke nieuwe query een kans om het
opnieuw fout te doen.

ZES schrijfwegen, niet vier: naast ap_timeline, ap_interactions,
ap_outbox en ap_mentions bleken ook guardianship/delivery (directe notes)
en de archief-import in dezelfde kolommen te schrijven. Alle zes leveren
nu ISO via NU_ISO uit config/database.js, waar isoSql ook staat -- wat je
schrijft en waarmee je vergelijkt horen bij elkaar en dus op een plek.

MIGRATIE voor wat er al stond, in het bestaande idempotente patroon in
initializeDatabase: alleen rijen in de CURRENT_TIMESTAMP-vorm, alleen als
strftime ze begrijpt. Een ISO-stempel blijft ongemoeid en een onleesbare
waarde wordt niet weggegooid -- die bewaar je, ook al weet je er niets
mee.

VIER VERGELIJKINGEN die het omgekeerde deden: een kolom RAUW tegen
datetime('now') leggen. Dat is dezelfde fout gespiegeld, en hij zou juist
door deze release gaan bijten. Het gaat om de wachtwoord-reset (de bead
noemt shaer-1evq), het opruimen van geziene notes en twee queries in de
sessieopslag -- de bead schreef al dat daar de opruiming en de telling
ernaast zaten. Alle vier nu datetime(kolom) tegen datetime('now'),
hetzelfde patroon dat Scheduler al gebruikt.

BEWUST NIET: de 49 DEFAULT CURRENT_TIMESTAMP in het schema. Die vragen om
een tabel-herbouw per stuk, en ze vuren alleen als een INSERT de kolom
overslaat -- wat bij de tabellen die het betreft niet gebeurt, want daar
staat created_at expliciet in het statement.

Drie toetsen erbij, elk met eigen tegenbewijs. Een ervan moest overnieuw:
hij schreef zijn eigen ISO en kon dus niet falen op wat ik veranderde;
nu gaat hij door upsertBoostedNote, de echte schrijfweg. Onderweg zette
ik SQL met enkele quotes in een JS-string met enkele quotes -- vandaar
NU_ISO als constante in template literals, en node --check op elk
aangeraakt bestand.

Volle suite 1255 groen.

  • Property mode set to 100644
File size: 3.6 KB
Line 
1/**
2 * Persistent session store backed by the existing `sessions` table in
3 * the SQLite DB. Survives `node --watch` restarts (no more "had to log in
4 * again after every save").
5 *
6 * Schema (already in migrations/001-init.sql):
7 * sessions(sid TEXT PRIMARY KEY, data TEXT, expiresAt DATETIME)
8 *
9 * Implements the minimal express-session Store contract: get / set /
10 * destroy / touch / length / clear / all. Periodic GC clears expired rows.
11 */
12
13import session from 'express-session';
14import db from '../config/database.js';
15
16const Store = session.Store;
17
18export class SqliteSessionStore extends Store {
19 constructor(options = {}) {
20 super(options);
21 // Periodic cleanup of expired rows. Default every 15 min.
22 const intervalMs = options.cleanupIntervalMs ?? 15 * 60 * 1000;
23 if (intervalMs > 0) {
24 this._gcTimer = setInterval(() => this._gc(), intervalMs);
25 // Don't keep the event loop alive just for GC.
26 if (this._gcTimer.unref) this._gcTimer.unref();
27 }
28
29 // Prepared statements (faster than re-preparing per call)
30 this._stmtGet = db.prepare('SELECT data, expiresAt FROM sessions WHERE sid = ?');
31 this._stmtSet = db.prepare(
32 'INSERT INTO sessions (sid, data, expiresAt) VALUES (?, ?, ?) ' +
33 'ON CONFLICT(sid) DO UPDATE SET data = excluded.data, expiresAt = excluded.expiresAt'
34 );
35 this._stmtDestroy = db.prepare('DELETE FROM sessions WHERE sid = ?');
36 this._stmtTouch = db.prepare('UPDATE sessions SET expiresAt = ? WHERE sid = ?');
37 this._stmtCount = db.prepare("SELECT COUNT(*) AS c FROM sessions WHERE datetime(expiresAt) > datetime('now')");
38 this._stmtClear = db.prepare('DELETE FROM sessions');
39 this._stmtAll = db.prepare("SELECT sid, data FROM sessions WHERE datetime(expiresAt) > datetime('now')");
40 this._stmtGc = db.prepare("DELETE FROM sessions WHERE datetime(expiresAt) <= datetime('now')");
41 }
42
43 _gc() {
44 try { this._stmtGc.run(); } catch (e) { /* swallow */ }
45 }
46
47 _expiryFor(sess) {
48 // express-session sets sess.cookie.expires (Date) when maxAge is set.
49 // Fallback to "now + 30 days" if missing (matches our cookie maxAge default).
50 const exp = sess?.cookie?.expires
51 ? new Date(sess.cookie.expires)
52 : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
53 return exp.toISOString();
54 }
55
56 get(sid, cb) {
57 try {
58 const row = this._stmtGet.get(sid);
59 if (!row) return cb(null, null);
60 // Lazy expiry check (the periodic GC may not have run yet)
61 if (row.expiresAt && new Date(row.expiresAt).getTime() <= Date.now()) {
62 this._stmtDestroy.run(sid);
63 return cb(null, null);
64 }
65 cb(null, JSON.parse(row.data));
66 } catch (e) {
67 cb(e);
68 }
69 }
70
71 set(sid, sess, cb) {
72 try {
73 this._stmtSet.run(sid, JSON.stringify(sess), this._expiryFor(sess));
74 cb && cb(null);
75 } catch (e) {
76 cb && cb(e);
77 }
78 }
79
80 destroy(sid, cb) {
81 try {
82 this._stmtDestroy.run(sid);
83 cb && cb(null);
84 } catch (e) {
85 cb && cb(e);
86 }
87 }
88
89 touch(sid, sess, cb) {
90 try {
91 this._stmtTouch.run(this._expiryFor(sess), sid);
92 cb && cb(null);
93 } catch (e) {
94 cb && cb(e);
95 }
96 }
97
98 length(cb) {
99 try { cb(null, this._stmtCount.get().c); } catch (e) { cb(e); }
100 }
101
102 clear(cb) {
103 try { this._stmtClear.run(); cb && cb(null); } catch (e) { cb && cb(e); }
104 }
105
106 all(cb) {
107 try {
108 const rows = this._stmtAll.all();
109 const out = {};
110 for (const r of rows) {
111 try { out[r.sid] = JSON.parse(r.data); } catch (e) { /* skip */ }
112 }
113 cb(null, out);
114 } catch (e) { cb(e); }
115 }
116}
117
118export default SqliteSessionStore;
Note: See TracBrowser for help on using the repository browser.