@shawnstack/quickforge 1.7.9 → 1.7.11
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/dist/assets/{AgentProfilesPage-BSM5w3bn.js → AgentProfilesPage-Cvb1KTz5.js} +1 -1
- package/dist/assets/ChatPanelHost-C7zV-xE_.js +48 -0
- package/dist/assets/{CloudAccountSettingsPage-BJMrfgy9.js → CloudAccountSettingsPage-mAs_nbvw.js} +1 -1
- package/dist/assets/{PluginsPage-mJkTmVVV.js → PluginsPage-mLvwvTmL.js} +1 -1
- package/dist/assets/{ScheduledTasksPage-DJj_NLm9.js → ScheduledTasksPage-CuUW_9lq.js} +1 -1
- package/dist/assets/{SettingsWorkspacePage-7lQ0Wl63.js → SettingsWorkspacePage-0OINfEqK.js} +390 -390
- package/dist/assets/{ShareLinksSettingsPage-DjFq_Lnb.js → ShareLinksSettingsPage-DqeyJYoD.js} +1 -1
- package/dist/assets/{SharedConversationPage-BbuNpcpP.js → SharedConversationPage-C7CDEoX9.js} +1 -1
- package/dist/assets/TerminalDock-CZXcuMpq.js +2 -0
- package/dist/assets/{WorkspaceInspector-0i6x-diB.js → WorkspaceInspector-CNSBOEvP.js} +3 -3
- package/dist/assets/index-BgMvQW1b.js +66 -0
- package/dist/assets/index-CB1g2D0f.css +3 -0
- package/dist/assets/local-tools-Dopjg9xq.js +270 -0
- package/dist/assets/{mcp-servers-dialog-BG4b67ea.js → mcp-servers-dialog-B2lALtTE.js} +1 -1
- package/dist/assets/{skills-dialog-DIzZvQqW.js → skills-dialog-DYp35JM3.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/server/acp/server.mjs +3 -6
- package/server/agent-manager.mjs +125 -43
- package/server/auto-archive.mjs +55 -8
- package/server/auto-compaction.mjs +0 -2
- package/server/context-usage.mjs +9 -7
- package/server/index.mjs +172 -51
- package/server/lan-access-cutover.mjs +21 -4
- package/server/maintenance/downgrade-session-state-v1.mjs +50 -87
- package/server/maintenance/export-session-state-v1.mjs +5 -13
- package/server/routes/backup.mjs +8 -1
- package/server/routes/storage.mjs +85 -48
- package/server/scheduled-runs-cutover.mjs +15 -1
- package/server/session-index-service.mjs +57 -391
- package/server/session-persistence-lock.mjs +17 -6
- package/server/session-state-backup.mjs +60 -7
- package/server/session-state-import.mjs +157 -0
- package/server/session-state-maintenance.mjs +126 -0
- package/server/session-state-service.mjs +133 -194
- package/server/share-cutover.mjs +21 -4
- package/server/sqlite/database.mjs +142 -7
- package/server/sqlite/lan-access-repository.mjs +6 -4
- package/server/sqlite/migrations.mjs +109 -0
- package/server/sqlite/scheduled-task-runs-repository.mjs +6 -3
- package/server/sqlite/session-index-repository.mjs +59 -208
- package/server/sqlite/session-state-repository.mjs +359 -289
- package/server/sqlite/share-repository.mjs +56 -45
- package/server/startup-state.mjs +107 -0
- package/server/storage.mjs +240 -448
- package/server/utils/logger.mjs +15 -4
- package/server/utils/process-tree.mjs +14 -2
- package/dist/assets/ChatPanelHost-cd80S4EX.js +0 -48
- package/dist/assets/TerminalDock-CczcCJJE.js +0 -2
- package/dist/assets/index-D-J_8Smf.css +0 -3
- package/dist/assets/index-DFASc15l.js +0 -66
- package/dist/assets/local-tools-Dn1Y9aPe.js +0 -270
- package/server/session-state-cutover.mjs +0 -370
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
1
2
|
import { getSqliteStorage } from './database.mjs'
|
|
2
3
|
|
|
4
|
+
// Storage v2 session index query layer. The dedicated `session_index`
|
|
5
|
+
// projection table is retired (renamed away by migration v11): the
|
|
6
|
+
// authoritative `sessions` table carries the promoted list columns
|
|
7
|
+
// (created_at, message_count, archived_at, pinned_at, meta_json) and this
|
|
8
|
+
// repository is now a read-only LIMIT/OFFSET query facade over it. There is
|
|
9
|
+
// nothing to sync, shadow or rebuild — the store and the index are the same
|
|
10
|
+
// rows.
|
|
11
|
+
|
|
12
|
+
// Sorting by lastModified stays metadata-driven: sessions.updated_at is the
|
|
13
|
+
// wall-clock write time, while the list UI (and the retired index) order by
|
|
14
|
+
// metadata.lastModified, which sessions may legitimately backdate.
|
|
15
|
+
const LAST_MODIFIED_EXPR = "json_extract(meta_json, '$.lastModified')"
|
|
16
|
+
|
|
3
17
|
const SELECT_COLUMNS = `
|
|
4
|
-
scope, project_id, session_id, created_at,
|
|
5
|
-
|
|
6
|
-
|
|
18
|
+
scope, project_id, session_id, created_at, updated_at,
|
|
19
|
+
${LAST_MODIFIED_EXPR} AS last_modified, message_count,
|
|
20
|
+
pinned_at, archived_at, state_version, meta_json
|
|
7
21
|
`
|
|
8
22
|
|
|
9
23
|
function assertStorage(storage) {
|
|
@@ -18,89 +32,6 @@ function assertNonEmptyString(value, field) {
|
|
|
18
32
|
return value
|
|
19
33
|
}
|
|
20
34
|
|
|
21
|
-
function normalizeBucket(scope, projectId) {
|
|
22
|
-
if (scope === 'global') return { scope, projectId: '' }
|
|
23
|
-
if (scope !== 'project') throw new TypeError('scope must be global or project')
|
|
24
|
-
return { scope, projectId: assertNonEmptyString(projectId, 'projectId') }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function assertPlainObject(value, field) {
|
|
28
|
-
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
29
|
-
throw new TypeError(`${field} must be a plain object`)
|
|
30
|
-
}
|
|
31
|
-
return value
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function nullableString(value, field) {
|
|
35
|
-
if (value === undefined || value === null) return null
|
|
36
|
-
if (typeof value !== 'string') throw new TypeError(`${field} must be a string or null`)
|
|
37
|
-
return value
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function nullableInteger(value, field) {
|
|
41
|
-
if (value === undefined || value === null) return null
|
|
42
|
-
if (!Number.isInteger(value) || value < 0) throw new TypeError(`${field} must be a non-negative integer or null`)
|
|
43
|
-
return value
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function normalizeRow(row) {
|
|
47
|
-
if (!row || typeof row !== 'object' || Array.isArray(row)) throw new TypeError('row must be an object')
|
|
48
|
-
const bucket = normalizeBucket(row.scope, row.projectId)
|
|
49
|
-
const metadata = assertPlainObject(row.metadata, 'metadata')
|
|
50
|
-
let metadataJson
|
|
51
|
-
try {
|
|
52
|
-
metadataJson = JSON.stringify(metadata)
|
|
53
|
-
} catch {
|
|
54
|
-
throw new TypeError('metadata must be JSON-serializable')
|
|
55
|
-
}
|
|
56
|
-
if (typeof row.metadataDigest !== 'string' || !/^[0-9a-f]{64}$/.test(row.metadataDigest)) {
|
|
57
|
-
throw new TypeError('metadataDigest must be a lowercase SHA-256 hex digest')
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
...bucket,
|
|
61
|
-
sessionId: assertNonEmptyString(row.sessionId, 'sessionId'),
|
|
62
|
-
createdAt: nullableString(row.createdAt, 'createdAt'),
|
|
63
|
-
lastModified: nullableString(row.lastModified, 'lastModified'),
|
|
64
|
-
messageCount: nullableInteger(row.messageCount, 'messageCount'),
|
|
65
|
-
pinnedAt: nullableString(row.pinnedAt, 'pinnedAt'),
|
|
66
|
-
archivedAt: nullableString(row.archivedAt, 'archivedAt'),
|
|
67
|
-
isPinned: row.pinnedAt ? 1 : 0,
|
|
68
|
-
isArchived: row.archivedAt ? 1 : 0,
|
|
69
|
-
stateVersion: nullableInteger(row.stateVersion, 'stateVersion'),
|
|
70
|
-
metadataJson,
|
|
71
|
-
metadataDigest: row.metadataDigest,
|
|
72
|
-
indexedAt: assertNonEmptyString(row.indexedAt, 'indexedAt'),
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function mapRow(row) {
|
|
77
|
-
if (!row) return null
|
|
78
|
-
return {
|
|
79
|
-
scope: row.scope,
|
|
80
|
-
projectId: row.scope === 'project' ? row.project_id : null,
|
|
81
|
-
sessionId: row.session_id,
|
|
82
|
-
createdAt: row.created_at,
|
|
83
|
-
lastModified: row.last_modified,
|
|
84
|
-
messageCount: row.message_count,
|
|
85
|
-
pinnedAt: row.pinned_at,
|
|
86
|
-
archivedAt: row.archived_at,
|
|
87
|
-
isPinned: row.is_pinned === 1,
|
|
88
|
-
isArchived: row.is_archived === 1,
|
|
89
|
-
stateVersion: row.state_version,
|
|
90
|
-
metadata: JSON.parse(row.metadata_json),
|
|
91
|
-
metadataDigest: row.metadata_digest,
|
|
92
|
-
indexedAt: row.indexed_at,
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function rowParameters(row) {
|
|
97
|
-
return [
|
|
98
|
-
row.scope, row.projectId, row.sessionId, row.createdAt, row.lastModified,
|
|
99
|
-
row.messageCount, row.pinnedAt, row.archivedAt, row.isPinned, row.isArchived,
|
|
100
|
-
row.stateVersion, row.metadataJson, row.metadataDigest, row.indexedAt,
|
|
101
|
-
]
|
|
102
|
-
}
|
|
103
|
-
|
|
104
35
|
function normalizeQuery(options = {}) {
|
|
105
36
|
const scopeMode = options.scopeMode ?? 'all'
|
|
106
37
|
if (!['all', 'global', 'project', 'projects'].includes(scopeMode)) throw new TypeError('scopeMode is invalid')
|
|
@@ -118,33 +49,56 @@ function normalizeQuery(options = {}) {
|
|
|
118
49
|
return { scopeMode, projectId, archive, pinnedOnly: options.pinnedOnly === true, sort, direction, limit, offset }
|
|
119
50
|
}
|
|
120
51
|
|
|
121
|
-
const SORT_COLUMNS = Object.freeze({ createdAt: 'created_at', lastModified:
|
|
52
|
+
const SORT_COLUMNS = Object.freeze({ createdAt: 'created_at', lastModified: LAST_MODIFIED_EXPR, pinnedAt: 'pinned_at' })
|
|
122
53
|
|
|
123
54
|
function buildQuery(options, { page = true } = {}) {
|
|
124
55
|
const query = normalizeQuery(options)
|
|
125
|
-
const where = ['
|
|
56
|
+
const where = ['message_count <> 0']
|
|
126
57
|
const parameters = []
|
|
127
58
|
if (query.scopeMode === 'global') where.push("scope = 'global' AND project_id = ''")
|
|
128
59
|
else if (query.scopeMode === 'project') {
|
|
129
60
|
where.push("scope = 'project' AND project_id = ?")
|
|
130
61
|
parameters.push(query.projectId)
|
|
131
62
|
} else if (query.scopeMode === 'projects') where.push("scope = 'project'")
|
|
132
|
-
if (query.archive === 'exclude') where.push('
|
|
133
|
-
else if (query.archive === 'only') where.push('
|
|
134
|
-
if (query.pinnedOnly) where.push('
|
|
63
|
+
if (query.archive === 'exclude') where.push('archived_at IS NULL')
|
|
64
|
+
else if (query.archive === 'only') where.push('archived_at IS NOT NULL')
|
|
65
|
+
if (query.pinnedOnly) where.push('pinned_at IS NOT NULL')
|
|
135
66
|
|
|
136
67
|
const column = SORT_COLUMNS[query.sort]
|
|
68
|
+
// Pinned-first ordering (metadata pinnedAt promoted to the pinned_at
|
|
69
|
+
// column): DESC on `pinned_at IS NOT NULL` matches the retired index's
|
|
70
|
+
// is_pinned DESC, and DESC on pinned_at itself keeps NULLs last.
|
|
137
71
|
const order = query.sort === 'pinnedAt'
|
|
138
72
|
? `${column} ${query.direction.toUpperCase()}`
|
|
139
|
-
: `
|
|
73
|
+
: `(pinned_at IS NOT NULL) DESC, pinned_at DESC, ${column} ${query.direction.toUpperCase()}`
|
|
140
74
|
const whereSql = `WHERE ${where.join(' AND ')}`
|
|
141
75
|
return {
|
|
142
76
|
query,
|
|
143
77
|
parameters,
|
|
144
78
|
whereSql,
|
|
145
79
|
order,
|
|
146
|
-
rowsSql: `SELECT ${SELECT_COLUMNS} FROM
|
|
147
|
-
countSql: `SELECT COUNT(*) AS count FROM
|
|
80
|
+
rowsSql: `SELECT ${SELECT_COLUMNS} FROM sessions ${whereSql} ORDER BY ${order}${page ? ' LIMIT ? OFFSET ?' : ''}`,
|
|
81
|
+
countSql: `SELECT COUNT(*) AS count FROM sessions ${whereSql}`,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mapRow(row) {
|
|
86
|
+
if (!row) return null
|
|
87
|
+
return {
|
|
88
|
+
scope: row.scope,
|
|
89
|
+
projectId: row.scope === 'project' ? row.project_id : null,
|
|
90
|
+
sessionId: row.session_id,
|
|
91
|
+
createdAt: row.created_at,
|
|
92
|
+
lastModified: row.last_modified ?? null,
|
|
93
|
+
messageCount: row.message_count,
|
|
94
|
+
pinnedAt: row.pinned_at ?? null,
|
|
95
|
+
archivedAt: row.archived_at ?? null,
|
|
96
|
+
isPinned: row.pinned_at != null,
|
|
97
|
+
isArchived: row.archived_at != null,
|
|
98
|
+
stateVersion: row.state_version,
|
|
99
|
+
metadata: JSON.parse(row.meta_json),
|
|
100
|
+
metadataDigest: createHash('sha256').update(row.meta_json).digest('hex'),
|
|
101
|
+
indexedAt: row.updated_at,
|
|
148
102
|
}
|
|
149
103
|
}
|
|
150
104
|
|
|
@@ -152,121 +106,16 @@ function analyzeInto(database, options) {
|
|
|
152
106
|
const built = buildQuery(options, { page: false })
|
|
153
107
|
const aggregate = built.query.scopeMode === 'all' || built.query.scopeMode === 'projects'
|
|
154
108
|
const duplicateSessionIdCount = aggregate
|
|
155
|
-
? Number(database.prepare(`SELECT COUNT(*) AS count FROM (SELECT session_id FROM
|
|
109
|
+
? Number(database.prepare(`SELECT COUNT(*) AS count FROM (SELECT session_id FROM sessions ${built.whereSql} GROUP BY session_id HAVING COUNT(*) > 1)`).get(...built.parameters).count)
|
|
156
110
|
: 0
|
|
157
111
|
const column = SORT_COLUMNS[built.query.sort]
|
|
158
|
-
const tieColumns = built.query.sort === 'pinnedAt' ? column : `
|
|
159
|
-
const fullSortKeyTieCount = Number(database.prepare(`SELECT COUNT(*) AS count FROM (SELECT ${tieColumns} FROM
|
|
112
|
+
const tieColumns = built.query.sort === 'pinnedAt' ? column : `(pinned_at IS NOT NULL), pinned_at, ${column}`
|
|
113
|
+
const fullSortKeyTieCount = Number(database.prepare(`SELECT COUNT(*) AS count FROM (SELECT ${tieColumns} FROM sessions ${built.whereSql} GROUP BY ${tieColumns} HAVING COUNT(*) > 1)`).get(...built.parameters).count)
|
|
160
114
|
return { duplicateSessionIdCount, fullSortKeyTieCount }
|
|
161
115
|
}
|
|
162
116
|
|
|
163
117
|
export function createSessionIndexRepository(storageHandle) {
|
|
164
118
|
const storage = assertStorage(storageHandle ?? getSqliteStorage())
|
|
165
|
-
const upsertSql = `
|
|
166
|
-
INSERT INTO session_index (${SELECT_COLUMNS}) VALUES (${new Array(14).fill('?').join(', ')})
|
|
167
|
-
ON CONFLICT(scope, project_id, session_id) DO UPDATE SET
|
|
168
|
-
created_at = excluded.created_at,
|
|
169
|
-
last_modified = excluded.last_modified,
|
|
170
|
-
message_count = excluded.message_count,
|
|
171
|
-
pinned_at = excluded.pinned_at,
|
|
172
|
-
archived_at = excluded.archived_at,
|
|
173
|
-
is_pinned = excluded.is_pinned,
|
|
174
|
-
is_archived = excluded.is_archived,
|
|
175
|
-
state_version = excluded.state_version,
|
|
176
|
-
metadata_json = excluded.metadata_json,
|
|
177
|
-
metadata_digest = excluded.metadata_digest,
|
|
178
|
-
indexed_at = excluded.indexed_at
|
|
179
|
-
`
|
|
180
|
-
|
|
181
|
-
function upsertInto(database, row) {
|
|
182
|
-
const normalized = normalizeRow(row)
|
|
183
|
-
database.prepare(upsertSql).run(...rowParameters(normalized))
|
|
184
|
-
return normalized
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function get(scope, projectId, sessionId) {
|
|
188
|
-
const bucket = normalizeBucket(scope, projectId)
|
|
189
|
-
assertNonEmptyString(sessionId, 'sessionId')
|
|
190
|
-
return mapRow(storage.prepare(`SELECT ${SELECT_COLUMNS} FROM session_index WHERE scope = ? AND project_id = ? AND session_id = ?`)
|
|
191
|
-
.get(bucket.scope, bucket.projectId, sessionId))
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function upsert(row) {
|
|
195
|
-
const normalized = upsertInto(storage, row)
|
|
196
|
-
return get(normalized.scope, normalized.projectId, normalized.sessionId)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function deleteRow(scope, projectId, sessionId) {
|
|
200
|
-
const bucket = normalizeBucket(scope, projectId)
|
|
201
|
-
assertNonEmptyString(sessionId, 'sessionId')
|
|
202
|
-
return storage.prepare('DELETE FROM session_index WHERE scope = ? AND project_id = ? AND session_id = ?')
|
|
203
|
-
.run(bucket.scope, bucket.projectId, sessionId).changes > 0
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function applyChanges({ upserts = [], deletes = [] } = {}) {
|
|
207
|
-
if (!Array.isArray(upserts) || !Array.isArray(deletes)) throw new TypeError('upserts and deletes must be arrays')
|
|
208
|
-
const normalizedUpserts = upserts.map(normalizeRow)
|
|
209
|
-
const normalizedDeletes = deletes.map((entry) => ({
|
|
210
|
-
...normalizeBucket(entry?.scope, entry?.projectId),
|
|
211
|
-
sessionId: assertNonEmptyString(entry?.sessionId, 'sessionId'),
|
|
212
|
-
}))
|
|
213
|
-
return storage.transaction((database) => {
|
|
214
|
-
const deleteStatement = database.prepare('DELETE FROM session_index WHERE scope = ? AND project_id = ? AND session_id = ?')
|
|
215
|
-
for (const entry of normalizedDeletes) deleteStatement.run(entry.scope, entry.projectId, entry.sessionId)
|
|
216
|
-
for (const row of normalizedUpserts) database.prepare(upsertSql).run(...rowParameters(row))
|
|
217
|
-
return { upserted: normalizedUpserts.length, deleted: normalizedDeletes.length }
|
|
218
|
-
})
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function replaceBucket(scope, projectId, rows) {
|
|
222
|
-
const bucket = normalizeBucket(scope, projectId)
|
|
223
|
-
if (!Array.isArray(rows)) throw new TypeError('rows must be an array')
|
|
224
|
-
const normalizedRows = rows.map((row) => normalizeRow({ ...row, ...bucket }))
|
|
225
|
-
return storage.transaction((database) => {
|
|
226
|
-
database.prepare('DELETE FROM session_index WHERE scope = ? AND project_id = ?').run(bucket.scope, bucket.projectId)
|
|
227
|
-
for (const row of normalizedRows) database.prepare(upsertSql).run(...rowParameters(row))
|
|
228
|
-
return normalizedRows.length
|
|
229
|
-
})
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function replaceAll(rows) {
|
|
233
|
-
if (!Array.isArray(rows)) throw new TypeError('rows must be an array')
|
|
234
|
-
const normalizedRows = rows.map(normalizeRow)
|
|
235
|
-
const keys = new Set()
|
|
236
|
-
for (const row of normalizedRows) {
|
|
237
|
-
const key = `${row.scope}\0${row.projectId}\0${row.sessionId}`
|
|
238
|
-
if (keys.has(key)) throw new TypeError('Duplicate session index composite key')
|
|
239
|
-
keys.add(key)
|
|
240
|
-
}
|
|
241
|
-
return storage.transaction((database) => {
|
|
242
|
-
database.exec('DELETE FROM session_index')
|
|
243
|
-
for (const row of normalizedRows) database.prepare(upsertSql).run(...rowParameters(row))
|
|
244
|
-
return normalizedRows.length
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function count() {
|
|
249
|
-
return Number(storage.prepare('SELECT COUNT(*) AS count FROM session_index').get().count)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function listVerification() {
|
|
253
|
-
return storage.prepare(`SELECT scope, project_id, session_id, metadata_digest FROM session_index ORDER BY scope, project_id, session_id`)
|
|
254
|
-
.all()
|
|
255
|
-
.map((row) => ({
|
|
256
|
-
scope: row.scope,
|
|
257
|
-
projectId: row.scope === 'project' ? row.project_id : null,
|
|
258
|
-
sessionId: row.session_id,
|
|
259
|
-
metadataDigest: row.metadata_digest,
|
|
260
|
-
}))
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function listIntegrity() {
|
|
264
|
-
return storage.prepare(`SELECT ${SELECT_COLUMNS} FROM session_index ORDER BY scope, project_id, session_id`).all().map(mapRow)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function listAll() {
|
|
268
|
-
return storage.prepare(`SELECT ${SELECT_COLUMNS} FROM session_index ORDER BY scope, project_id, session_id`).all().map(mapRow)
|
|
269
|
-
}
|
|
270
119
|
|
|
271
120
|
function listPage(options) {
|
|
272
121
|
const built = buildQuery(options)
|
|
@@ -277,10 +126,6 @@ export function createSessionIndexRepository(storageHandle) {
|
|
|
277
126
|
}, { mode: 'deferred' })
|
|
278
127
|
}
|
|
279
128
|
|
|
280
|
-
function readMetadataEntries() {
|
|
281
|
-
return storage.prepare('SELECT metadata_json FROM session_index').all().map((row) => JSON.parse(row.metadata_json))
|
|
282
|
-
}
|
|
283
|
-
|
|
284
129
|
function analyzeQuery(options) {
|
|
285
130
|
return analyzeInto(storage, options)
|
|
286
131
|
}
|
|
@@ -290,8 +135,14 @@ export function createSessionIndexRepository(storageHandle) {
|
|
|
290
135
|
return storage.prepare(`EXPLAIN QUERY PLAN ${built.rowsSql}`).all(...built.parameters, built.query.limit, built.query.offset)
|
|
291
136
|
}
|
|
292
137
|
|
|
138
|
+
function count() {
|
|
139
|
+
return Number(storage.prepare('SELECT COUNT(*) AS count FROM sessions').get().count)
|
|
140
|
+
}
|
|
141
|
+
|
|
293
142
|
return Object.freeze({
|
|
294
|
-
|
|
295
|
-
|
|
143
|
+
listPage,
|
|
144
|
+
analyzeQuery,
|
|
145
|
+
explainQueryPlan,
|
|
146
|
+
count,
|
|
296
147
|
})
|
|
297
148
|
}
|