@andrian.yablonskyy/thub-coordinator 1.0.37 → 1.0.39
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/bin/thub-admin.js +3 -5
- package/config.json +1 -6
- package/package.json +2 -2
- package/public/css/thub.css +129 -0
- package/public/js/config-import.js +37 -33
- package/public/js/copy-to-clipboard.js +21 -18
- package/public/js/help.js +93 -0
- package/public/js/list-search.js +51 -0
- package/public/js/live.js +287 -0
- package/public/js/log-viewer.js +289 -45
- package/public/js/remove-resource.js +5 -3
- package/public/js/resource-card.js +14 -13
- package/public/js/tooltips.js +3 -1
- package/scripts/install-default-config.js +1 -1
- package/scripts/install-target.js +2 -2
- package/src/api/admin.js +1 -1
- package/src/api/agent.js +4 -10
- package/src/api/resource.js +6 -11
- package/src/api/sse.js +13 -10
- package/src/config.js +16 -9
- package/src/db/migrations/022_add_sessions.sql +10 -0
- package/src/db/migrations/023_drop_artifacts.sql +4 -0
- package/src/dev/virtual.js +1 -1
- package/src/server.js +55 -21
- package/src/services/cleanup.js +2 -2
- package/src/services/jobs.js +33 -19
- package/src/services/list-prefs.js +4 -0
- package/src/services/live.js +124 -0
- package/src/services/logs.js +12 -14
- package/src/services/retention.js +5 -14
- package/src/services/search.js +59 -0
- package/src/services/session-store.js +127 -0
- package/src/web/routes.js +205 -34
- package/test/cleanup.test.js +1 -5
- package/test/list-prefs.test.js +6 -0
- package/test/live.test.js +153 -0
- package/test/logs.test.js +150 -0
- package/test/scheduler.test.js +10 -12
- package/test/search.test.js +97 -0
- package/test/session.test.js +132 -0
- package/test/views.test.js +30 -0
- package/views/admin/agents.pug +28 -10
- package/views/groups/list.pug +14 -1
- package/views/help/_agent-cli.pug +103 -0
- package/views/help/_agent-setup.pug +72 -0
- package/views/help/_ci.pug +44 -0
- package/views/help/_client-machines.pug +87 -0
- package/views/help/_client-setup.pug +122 -0
- package/views/help/_coordinator.pug +137 -0
- package/views/help/_docker.pug +124 -0
- package/views/help/_env.pug +127 -0
- package/views/help/_git.pug +130 -0
- package/views/help/_mixins.pug +24 -0
- package/views/help/_overview.pug +118 -0
- package/views/help/_quick-start.pug +37 -0
- package/views/help/_troubleshooting.pug +44 -0
- package/views/help/index.pug +67 -0
- package/views/index.pug +3 -2
- package/views/jobs/list.pug +8 -5
- package/views/jobs/show.pug +0 -10
- package/views/layout.pug +24 -1
- package/views/mixins/list-controls.pug +36 -0
- package/views/mixins/log-viewer.pug +39 -3
- package/views/mixins/resource.pug +1 -1
- package/views/resources/list.pug +7 -4
- package/src/services/artifacts.js +0 -149
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/coordinator/src/services/live.js
|
|
3
|
+
* @description Dashboard live updates: detects when what a list page shows (resources, agents, jobs) has changed and notifies subscribers
|
|
4
|
+
*
|
|
5
|
+
* @author Andrian Yablonskyy
|
|
6
|
+
* @copyright Copyright (c) 2026 Andrian Yablonskyy. All rights reserved.
|
|
7
|
+
*
|
|
8
|
+
* This file is part of TestHub and is proprietary and confidential.
|
|
9
|
+
* Unauthorized copying, modification, distribution, or use of this file,
|
|
10
|
+
* via any medium, is strictly prohibited without prior written permission
|
|
11
|
+
* from AdSystem.PRO.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const crypto = require('node:crypto'),
|
|
17
|
+
{ ACTIVE_JOB_STATES } = require('@andrian.yablonskyy/thub-common');
|
|
18
|
+
|
|
19
|
+
// How the dashboard stays current (README §10): the page holds one
|
|
20
|
+
// GET /live stream that only says *which* topics changed; it then re-fetches
|
|
21
|
+
// itself and swaps the parts that differ (public/js/live.js). Rather than
|
|
22
|
+
// every place that changes a resource or agent having to announce it, a
|
|
23
|
+
// fingerprint of each topic is recomputed and compared — right away on bus
|
|
24
|
+
// events (a job starting or finishing shows up at once) and every
|
|
25
|
+
// `intervalMs` as the catch-all for everything else (heartbeats, the
|
|
26
|
+
// sweeper, maintenance, renames, update requests, …).
|
|
27
|
+
const TOPICS = ['resources', 'agents', 'jobs'],
|
|
28
|
+
BUS_EVENTS = ['job.queued', 'job.assigned', 'job.state', 'job.finished', 'resource.idle', 'resource.reregistered'];
|
|
29
|
+
|
|
30
|
+
function createLiveService(db, { bus, updates, intervalMs = 5000, debounceMs = 250 } = {}){
|
|
31
|
+
const subscribers = new Set(),
|
|
32
|
+
activeStates = [...ACTIVE_JOB_STATES],
|
|
33
|
+
hash = (value) => crypto.createHash('sha1').update(JSON.stringify(value)).digest('base64'),
|
|
34
|
+
// One cheap query per topic covering everything its pages display.
|
|
35
|
+
// Resources: every column (tens of rows) — last_heartbeat_at included,
|
|
36
|
+
// since the pages show it. Jobs: counts and latest times of the whole
|
|
37
|
+
// table (new, finished and deleted jobs) plus every active job's state.
|
|
38
|
+
queries = {
|
|
39
|
+
resources: () => [db.prepare('SELECT * FROM resources ORDER BY id').all(), updates?.status().latest],
|
|
40
|
+
agents: () => [
|
|
41
|
+
db.prepare('SELECT id, name, kind, version, update_to, last_used_at, revoked_at FROM agents ORDER BY id').all(),
|
|
42
|
+
updates?.status().latest
|
|
43
|
+
],
|
|
44
|
+
jobs: () => [
|
|
45
|
+
db.prepare('SELECT COUNT(*) AS n, MAX(created_at) AS created, MAX(finished_at) AS finished FROM jobs').get(),
|
|
46
|
+
db.prepare(
|
|
47
|
+
`SELECT id, state, resource_id, message FROM jobs WHERE state IN (${activeStates.map(() => '?').join(',')}) ORDER BY id`
|
|
48
|
+
).all(...activeStates)
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
let fingerprints = null,
|
|
52
|
+
timer = null,
|
|
53
|
+
pending = null;
|
|
54
|
+
|
|
55
|
+
function compute(){
|
|
56
|
+
return Object.fromEntries(TOPICS.map((t) => [t, hash(queries[t]())]));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Compare with the last fingerprints; tell every subscriber what changed.
|
|
60
|
+
function check(){
|
|
61
|
+
pending = null;
|
|
62
|
+
if (!subscribers.size){
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const next = compute(),
|
|
66
|
+
changed = fingerprints ? TOPICS.filter((t) => next[t] !== fingerprints[t]) : [];
|
|
67
|
+
fingerprints = next;
|
|
68
|
+
if (changed.length){
|
|
69
|
+
for (const fn of subscribers){
|
|
70
|
+
try {
|
|
71
|
+
fn(changed);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// one broken subscriber (a closed stream) mustn't stop the others
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return changed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Bus events arrive in bursts (state + log + finished): check once.
|
|
82
|
+
function soon(){
|
|
83
|
+
if (subscribers.size && !pending){
|
|
84
|
+
pending = setTimeout(check, debounceMs);
|
|
85
|
+
pending.unref?.();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function start(){
|
|
90
|
+
fingerprints = compute();
|
|
91
|
+
timer = setInterval(check, intervalMs);
|
|
92
|
+
timer.unref?.();
|
|
93
|
+
for (const e of BUS_EVENTS){
|
|
94
|
+
bus.on(e, soon);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stop(){
|
|
99
|
+
clearInterval(timer);
|
|
100
|
+
clearTimeout(pending);
|
|
101
|
+
timer = pending = null;
|
|
102
|
+
fingerprints = null;
|
|
103
|
+
for (const e of BUS_EVENTS){
|
|
104
|
+
bus.off(e, soon);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Nothing runs while no dashboard is open.
|
|
109
|
+
function subscribe(fn){
|
|
110
|
+
subscribers.add(fn);
|
|
111
|
+
if (subscribers.size === 1){
|
|
112
|
+
start();
|
|
113
|
+
}
|
|
114
|
+
return () => {
|
|
115
|
+
if (subscribers.delete(fn) && !subscribers.size){
|
|
116
|
+
stop();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { subscribe, check, subscriberCount: () => subscribers.size, TOPICS };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { createLiveService, TOPICS };
|
package/src/services/logs.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
// §3.1 Log service: accepts batched log lines, assigns a monotonically
|
|
17
17
|
// increasing seq per job, stores them, and fans them out over the bus for
|
|
18
|
-
// SSE subscribers (§6.4).
|
|
18
|
+
// SSE subscribers (§6.4). Lines are deleted with their job (§9).
|
|
19
19
|
function createLogsService(db, { bus }){
|
|
20
20
|
const insert = db.prepare(
|
|
21
21
|
'INSERT INTO job_logs (job_id, seq, ts, stream, line) VALUES (?, ?, ?, ?, ?)'
|
|
@@ -47,24 +47,22 @@ function createLogsService(db, { bus }){
|
|
|
47
47
|
.all(jobId, afterSeq, limit);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
|
|
50
|
+
// The dashboard log viewer's pages (views/mixins/log-viewer.pug): the
|
|
51
|
+
// `limit` lines just before `beforeSeq` (the end of the log when omitted),
|
|
52
|
+
// oldest first, and whether there are earlier ones to scroll up to.
|
|
53
|
+
function pageBefore(jobId, beforeSeq, limit){
|
|
52
54
|
const rows = db
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
.prepare('SELECT seq, ts, stream, line FROM job_logs WHERE job_id = ? AND seq < ? ORDER BY seq DESC LIMIT ?')
|
|
56
|
+
.all(jobId, beforeSeq ?? Number.MAX_SAFE_INTEGER, limit + 1),
|
|
57
|
+
hasMore = rows.length > limit;
|
|
58
|
+
return { lines: rows.slice(0, limit).reverse(), hasMore };
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
db.prepare(
|
|
61
|
-
`DELETE FROM job_logs WHERE job_id IN (
|
|
62
|
-
SELECT id FROM jobs WHERE finished_at IS NOT NULL AND finished_at <= ?
|
|
63
|
-
)`
|
|
64
|
-
).run(cutoff);
|
|
61
|
+
function count(jobId){
|
|
62
|
+
return db.prepare('SELECT COUNT(*) AS n FROM job_logs WHERE job_id = ?').get(jobId).n;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
return { appendBatch, listSince,
|
|
65
|
+
return { appendBatch, listSince, pageBefore, count };
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
module.exports = { createLogsService };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file packages/coordinator/src/services/retention.js
|
|
3
|
-
* @description
|
|
3
|
+
* @description Nightly consistent database backup (VACUUM INTO, README §9)
|
|
4
4
|
*
|
|
5
5
|
* @author Andrian Yablonskyy
|
|
6
6
|
* @copyright Copyright (c) 2026 Andrian Yablonskyy. All rights reserved.
|
|
@@ -17,20 +17,11 @@ const path = require('node:path');
|
|
|
17
17
|
|
|
18
18
|
const DAY_MS = 86400 * 1000;
|
|
19
19
|
|
|
20
|
-
// §9:
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
function createRetentionService(db, {
|
|
24
|
-
bus.on('job.finished', ({ jobId }) => {
|
|
25
|
-
const text = logs.renderConsoleLog(jobId);
|
|
26
|
-
if (text){
|
|
27
|
-
artifacts.storeGenerated(jobId, 'console.log', Buffer.from(text, 'utf8'), 'text/plain');
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
20
|
+
// §9: a nightly consistent backup via VACUUM INTO. (A job's log lines live
|
|
21
|
+
// as long as the job itself — retention.jobRetention / Clean up database,
|
|
22
|
+
// services/cleanup.js — and there are no artifacts to purge any more.)
|
|
23
|
+
function createRetentionService(db, { config }){
|
|
31
24
|
function runDaily(){
|
|
32
|
-
logs.purgeOlderThan(config.retention.logRetentionDays);
|
|
33
|
-
artifacts.purgeOlderThan(config.retention.artifactRetentionDays);
|
|
34
25
|
const backupPath = path.join(config.dataDir, `backup-${new Date().toISOString().slice(0, 10)}.db`);
|
|
35
26
|
db.exec(`VACUUM INTO '${backupPath.replace(/'/g, '\'\'')}'`);
|
|
36
27
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/coordinator/src/services/search.js
|
|
3
|
+
* @description Dashboard search (?q=): splits the query into words; a record matches when every word occurs in one of its fields
|
|
4
|
+
*
|
|
5
|
+
* @author Andrian Yablonskyy
|
|
6
|
+
* @copyright Copyright (c) 2026 Andrian Yablonskyy. All rights reserved.
|
|
7
|
+
*
|
|
8
|
+
* This file is part of TestHub and is proprietary and confidential.
|
|
9
|
+
* Unauthorized copying, modification, distribution, or use of this file,
|
|
10
|
+
* via any medium, is strictly prohibited without prior written permission
|
|
11
|
+
* from AdSystem.PRO.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const MAX_QUERY = 200,
|
|
17
|
+
MAX_TERMS = 10;
|
|
18
|
+
|
|
19
|
+
// " Lab-HW nucleo " -> ['lab-hw', 'nucleo']. Case-insensitive, words in
|
|
20
|
+
// any order; capped so a pasted wall of text can't build a huge query.
|
|
21
|
+
function parseTerms(q){
|
|
22
|
+
if (typeof q !== 'string'){
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
return [...new Set(q.slice(0, MAX_QUERY).toLowerCase().split(/\s+/).filter(Boolean))].slice(0, MAX_TERMS);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The query as shown back in the search box (trimmed, capped).
|
|
29
|
+
function normalizeQuery(q){
|
|
30
|
+
return typeof q === 'string' ? q.slice(0, MAX_QUERY).trim() : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// In-memory lists (resources, groups, agents): `fields` are the record's
|
|
34
|
+
// searchable values — strings, numbers, arrays of them, or null.
|
|
35
|
+
function matches(fields, terms){
|
|
36
|
+
if (!terms.length){
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const text = fields.flat(Infinity).filter((v) => v !== null && v !== undefined && v !== '').join('\n').toLowerCase();
|
|
40
|
+
return terms.every((t) => text.includes(t));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// SQL lists (jobs): every term must match at least one of `expressions`
|
|
44
|
+
// (SQL expressions yielding text). LIKE is case-insensitive for ASCII in
|
|
45
|
+
// SQLite; % and _ in the query are matched literally.
|
|
46
|
+
function likeClause(expressions, terms){
|
|
47
|
+
if (!terms.length){
|
|
48
|
+
return { sql: '', params: [] };
|
|
49
|
+
}
|
|
50
|
+
const one = `(${expressions.map((e) => `COALESCE(${e}, '') LIKE ? ESCAPE '\\'`).join(' OR ')})`,
|
|
51
|
+
params = [];
|
|
52
|
+
for (const t of terms){
|
|
53
|
+
const pattern = `%${t.replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
54
|
+
params.push(...expressions.map(() => pattern));
|
|
55
|
+
}
|
|
56
|
+
return { sql: terms.map(() => one).join(' AND '), params };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { parseTerms, normalizeQuery, matches, likeClause, MAX_QUERY };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/coordinator/src/services/session-store.js
|
|
3
|
+
* @description Dashboard session store in the Coordinator's SQLite database: survives restarts, prunes expired sessions
|
|
4
|
+
*
|
|
5
|
+
* @author Andrian Yablonskyy
|
|
6
|
+
* @copyright Copyright (c) 2026 Andrian Yablonskyy. All rights reserved.
|
|
7
|
+
*
|
|
8
|
+
* This file is part of TestHub and is proprietary and confidential.
|
|
9
|
+
* Unauthorized copying, modification, distribution, or use of this file,
|
|
10
|
+
* via any medium, is strictly prohibited without prior written permission
|
|
11
|
+
* from AdSystem.PRO.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const session = require('express-session');
|
|
17
|
+
|
|
18
|
+
// express-session's default MemoryStore loses every session on a restart,
|
|
19
|
+
// never drops expired ones (memory only grows) and warns against itself in
|
|
20
|
+
// production. This one keeps them in the `sessions` table (migration 022),
|
|
21
|
+
// so signing in survives a Coordinator restart or self-update, and an
|
|
22
|
+
// expired session is deleted when read and by a periodic sweep.
|
|
23
|
+
const PRUNE_INTERVAL_MS = 15 * 60 * 1000,
|
|
24
|
+
// A session without a cookie expiry (never the case here: routes.js sets
|
|
25
|
+
// each user's idle timeout) is kept this long.
|
|
26
|
+
FALLBACK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
27
|
+
|
|
28
|
+
class SqliteSessionStore extends session.Store{
|
|
29
|
+
constructor(db, { pruneIntervalMs = PRUNE_INTERVAL_MS, now = Date.now } = {}){
|
|
30
|
+
super();
|
|
31
|
+
this.now = now;
|
|
32
|
+
this.stmts = {
|
|
33
|
+
get: db.prepare('SELECT sess, expires FROM sessions WHERE sid = ?'),
|
|
34
|
+
set: db.prepare(
|
|
35
|
+
`INSERT INTO sessions (sid, sess, expires) VALUES (?, ?, ?)
|
|
36
|
+
ON CONFLICT(sid) DO UPDATE SET sess = excluded.sess, expires = excluded.expires`
|
|
37
|
+
),
|
|
38
|
+
touch: db.prepare('UPDATE sessions SET sess = ?, expires = ? WHERE sid = ?'),
|
|
39
|
+
destroy: db.prepare('DELETE FROM sessions WHERE sid = ?'),
|
|
40
|
+
prune: db.prepare('DELETE FROM sessions WHERE expires <= ?'),
|
|
41
|
+
length: db.prepare('SELECT COUNT(*) AS n FROM sessions WHERE expires > ?'),
|
|
42
|
+
clear: db.prepare('DELETE FROM sessions')
|
|
43
|
+
};
|
|
44
|
+
if (pruneIntervalMs > 0){
|
|
45
|
+
this.pruneTimer = setInterval(() => this.prune(), pruneIntervalMs);
|
|
46
|
+
this.pruneTimer.unref?.();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
expiresOf(sess){
|
|
51
|
+
const at = sess?.cookie?.expires ? new Date(sess.cookie.expires).getTime() : NaN;
|
|
52
|
+
return Number.isFinite(at) ? at : this.now() + FALLBACK_TTL_MS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Each method answers through `cb`, as express-session expects; errors
|
|
56
|
+
// (a locked or broken database) go there too instead of throwing.
|
|
57
|
+
run(cb, fn){
|
|
58
|
+
let result;
|
|
59
|
+
try {
|
|
60
|
+
result = fn();
|
|
61
|
+
}
|
|
62
|
+
catch (err){
|
|
63
|
+
return cb?.(err);
|
|
64
|
+
}
|
|
65
|
+
cb?.(null, result);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get(sid, cb){
|
|
69
|
+
this.run(cb, () => {
|
|
70
|
+
const row = this.stmts.get.get(sid);
|
|
71
|
+
if (!row){
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (row.expires <= this.now()){
|
|
75
|
+
this.stmts.destroy.run(sid);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return JSON.parse(row.sess);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
set(sid, sess, cb){
|
|
83
|
+
this.run(cb, () => {
|
|
84
|
+
this.stmts.set.run(sid, JSON.stringify(sess), this.expiresOf(sess));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Called for a request that didn't change the session: moves its expiry
|
|
89
|
+
// (rolling idle timeout, §10.1). A passive live-update request leaves the
|
|
90
|
+
// cookie's expiry as it was (server.js), so this writes the same value.
|
|
91
|
+
touch(sid, sess, cb){
|
|
92
|
+
this.run(cb, () => {
|
|
93
|
+
this.stmts.touch.run(JSON.stringify(sess), this.expiresOf(sess), sid);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
destroy(sid, cb){
|
|
98
|
+
this.run(cb, () => {
|
|
99
|
+
this.stmts.destroy.run(sid);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
length(cb){
|
|
104
|
+
this.run(cb, () => this.stmts.length.get(this.now()).n);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
clear(cb){
|
|
108
|
+
this.run(cb, () => {
|
|
109
|
+
this.stmts.clear.run();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
prune(){
|
|
114
|
+
try {
|
|
115
|
+
return this.stmts.prune.run(this.now()).changes;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return 0; // the next sweep tries again
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
close(){
|
|
123
|
+
clearInterval(this.pruneTimer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { SqliteSessionStore };
|