src/vs/platform/agentHost/node/sessionDatabase.ts
781 LOC · 651 covered · 130 uncovered · 128 ranges · 974 concepts · 50 introducers · 509 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
sessionDatabase.ts ×50
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { SequencerByKey } from '../../../base/common/async.js';
import type { Database, RunResult } from '@vscode/sqlite3';
import type { IFileEditContent, IFileEditRecord, ILocalTurnRecord, IReviewedFileRecord, ISessionDatabase } from '../common/sessionDataService.js';
import { dirname } from '../../../base/common/path.js';
import { URI } from '../../../base/common/uri.js';
import type { Message } from '../common/state/sessionState.js';
/**
* A single numbered migration. Migrations are applied in order of
* {@link version} and tracked via `PRAGMA user_version`.
*/
export interface ISessionDatabaseMigration {
/** Monotonically-increasing version number (1-based). */
readonly version: number;
/** SQL to execute for this migration. */
readonly sql: string;
}
/**
* The set of migrations that define the current session database schema.
* New migrations should be **appended** to this array with the next version
* number. Never reorder or mutate existing entries.
*/
export const sessionDatabaseMigrations: readonly ISessionDatabaseMigration[] = [
{
version: 1,
sql: [
`CREATE TABLE IF NOT EXISTS turns (
id TEXT PRIMARY KEY NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS file_edits (
turn_id TEXT NOT NULL REFERENCES turns(id) ON DELETE CASCADE,
tool_call_id TEXT NOT NULL,
file_path TEXT NOT NULL,
before_content BLOB NOT NULL,
after_content BLOB NOT NULL,
added_lines INTEGER,
removed_lines INTEGER,
PRIMARY KEY (tool_call_id, file_path)
)`,
].join(';\n'),
},
{
version: 2,
sql: `CREATE TABLE IF NOT EXISTS session_metadata (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL
)`,
},
{
version: 3,
sql: [
// Recreate file_edits with new columns: edit_type, original_path,
// and nullable before_content/after_content.
`CREATE TABLE file_edits_v3 (
turn_id TEXT NOT NULL REFERENCES turns(id) ON DELETE CASCADE,
tool_call_id TEXT NOT NULL,
file_path TEXT NOT NULL,
edit_type TEXT NOT NULL DEFAULT 'edit',
original_path TEXT,
before_content BLOB,
after_content BLOB,
added_lines INTEGER,
removed_lines INTEGER,
PRIMARY KEY (tool_call_id, file_path)
)`,
`INSERT INTO file_edits_v3 (turn_id, tool_call_id, file_path, edit_type, before_content, after_content, added_lines, removed_lines)
SELECT turn_id, tool_call_id, file_path, 'edit', before_content, after_content, added_lines, removed_lines FROM file_edits`,
`DROP TABLE file_edits`,
`ALTER TABLE file_edits_v3 RENAME TO file_edits`,
].join(';\n'),
},
{
version: 4,
sql: [
`ALTER TABLE turns ADD COLUMN event_id TEXT`,
`CREATE INDEX IF NOT EXISTS idx_turns_event_id ON turns(event_id)`,
].join(';\n'),
},
{
version: 5,
sql: `ALTER TABLE turns ADD COLUMN checkpoint_ref TEXT`,
},
{
version: 6,
sql: `CREATE TABLE IF NOT EXISTS chat_drafts (
chat_uri TEXT PRIMARY KEY NOT NULL,
draft TEXT NOT NULL
)`,
},
{
version: 7,
sql: `CREATE TABLE IF NOT EXISTS reviewed_files (
uri TEXT NOT NULL,
nonce TEXT NOT NULL,
PRIMARY KEY (uri, nonce)
)`,
},
{
version: 8,
sql: `CREATE TABLE IF NOT EXISTS local_turns (
turn_id TEXT PRIMARY KEY NOT NULL,
chat_uri TEXT NOT NULL,
anchor_turn_id TEXT,
seq INTEGER NOT NULL,
payload TEXT NOT NULL
)`,
},
];
// ---- Promise wrappers around callback-based @vscode/sqlite3 API -----------
return new Promise((resolve, reject) => {
db.exec(sql, err => err ? reject(err) : resolve());
});
}
function dbRun(db: Database, sql: string, params: unknown[]): Promise<{ changes: number; lastID: number }> {
sessionDatabase.ts ×2
return new Promise((resolve, reject) => {
db.run(sql, params, function (this: RunResult, err: Error | null) {
if (err) {
return reject(err);
}
});
});
}
function dbGet(db: Database, sql: string, params: unknown[]): Promise<Record<string, unknown> | undefined> {
sessionDatabase.ts ×13
return new Promise((resolve, reject) => {
db.get(sql, params, (err: Error | null, row: Record<string, unknown> | undefined) => {
if (err) {
return reject(err);
}
});
});
}
function dbAll(db: Database, sql: string, params: unknown[]): Promise<Record<string, unknown>[]> {
sessionDatabase.ts ×2
return new Promise((resolve, reject) => {
db.all(sql, params, (err: Error | null, rows: Record<string, unknown>[]) => {
if (err) {
return reject(err);
}
});
});
}
return new Promise((resolve, reject) => {
db.close(err => err ? reject(err) : resolve());
});
}
return new Promise((resolve, reject) => {
import('@vscode/sqlite3').then(sqlite3 => {
const db = new sqlite3.default.Database(path, (err: Error | null) => {
if (err) {
return reject(err);
}
});
}, reject);
});
}
/**
* Applies any pending {@link ISessionDatabaseMigration migrations} to a
* database. Migrations whose version is greater than the current
* `PRAGMA user_version` are run inside a serialized transaction. After all
* migrations complete the pragma is updated to the highest applied version.
*/
export async function runMigrations(db: Database, migrations: readonly ISessionDatabaseMigration[]): Promise<void> {
sessionDatabase.ts ×13
// Enable foreign key enforcement — must be set outside a transaction
// and every time a connection is opened.
await dbExec(db, 'PRAGMA foreign_keys = ON');
const row = await dbGet(db, 'PRAGMA user_version', []);
const currentVersion = (row?.user_version as number | undefined) ?? 0;
const pending = migrations
.filter(m => m.version > currentVersion)
.sort((a, b) => a.version - b.version);
if (pending.length === 0) {
}
await dbExec(db, 'BEGIN TRANSACTION');
try {
for (const migration of pending) {
await dbExec(db, migration.sql);
// PRAGMA cannot be parameterized; the version is a trusted literal.
await dbExec(db, `PRAGMA user_version = ${migration.version}`);
}
await dbExec(db, 'COMMIT');
} catch (err) {
throw err;
}
/**
* A wrapper around a `@vscode/sqlite3` {@link Database} instance with
* lazy initialisation.
*
* The underlying connection is opened on the first async method call
* (not at construction time), allowing the object to be created
* synchronously and shared via a {@link ReferenceCollection}.
*
* Calling {@link dispose} closes the connection.
*/
export class SessionDatabase implements ISessionDatabase {
protected _dbPromise: Promise<Database> | undefined;
protected _closed: Promise<void> | true | undefined;
private readonly _fileEditSequencer = new SequencerByKey<string>();
/**
* Serializes `setMetadata` writes per key. `@vscode/sqlite3` runs in
* parallelized mode, so two `db.run()` calls on the same connection
* can be dispatched to the libuv thread pool and complete out of
* submission order. For "last writer wins" keys (notably `configValues`
* via {@link setMetadata}), that meant a fast-following second write
* could be overtaken by the first and silently lose its value — see
* the "Session Config persistence across restarts" integration test.
* Sequencing by key preserves intra-key order while still allowing
* writes for different keys to run concurrently.
*/
private readonly _metadataSequencer = new SequencerByKey<string>();
/**
* In-flight write operations. Tracked so {@link whenIdle} can await them
* before the process exits — without this, a `SIGTERM` arriving between
* a fire-and-forget mutating call (e.g. `setMetadata`) being invoked and
* its underlying SQLite query completing would silently drop the write.
* Every public mutating method routes its returned promise through
* {@link _track}; reads (`getMetadata`, `getFileEdits`, ...) skip
* tracking since shutdown does not need to wait for them.
*/
private readonly _pendingWrites = new Set<Promise<unknown>>();
constructor(
private readonly _migrations: readonly ISessionDatabaseMigration[] = sessionDatabaseMigrations,
) { }
/**
* Opens (or creates) a SQLite database at {@link path} and applies
* any pending migrations. Only used in tests where synchronous
* construction + immediate readiness is desired.
*/
static async open(path: string, migrations: readonly ISessionDatabaseMigration[] = sessionDatabaseMigrations): Promise<SessionDatabase> {
await inst._ensureDb();
return inst;
}
protected _ensureDb(): Promise<Database> {
}
this._dbPromise = (async () => {
// Ensure the parent directory exists before SQLite tries to
// create the database file.
await fs.promises.mkdir(dirname(this._path), { recursive: true });
const db = await dbOpen(this._path);
try {
await runMigrations(db, this._migrations);
} catch (err) {
this._dbPromise = undefined;
throw err;
}
if (this._closed) {
await dbClose(db);
throw new Error('SessionDatabase has been disposed');
}
})().catch(err => {
throw err;
}
return this._dbPromise;
/**
* Returns the names of all user-created tables in the database.
* Useful for testing migration behavior.
*/
async getAllTables(): Promise<string[]> {
const rows = await dbAll(db, `SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`, []);
return rows.map(r => r.name as string);
}
// ---- Turns ----------------------------------------------------------
createTurn(turnId: string): Promise<void> {
const db = await this._ensureDb();
await dbRun(db, 'INSERT OR IGNORE INTO turns (id) VALUES (?)', [turnId]);
sessionDatabase.ts ×1
}
deleteTurn(turnId: string): Promise<void> {
const db = await this._ensureDb();
await dbRun(db, 'DELETE FROM turns WHERE id = ?', [turnId]);
});
}
setTurnEventId(turnId: string, eventId: string): Promise<void> {
const db = await this._ensureDb();
await dbRun(db, 'INSERT OR IGNORE INTO turns (id) VALUES (?)', [turnId]);
// Only set the event ID if not already set — steering messages
// trigger additional user.message events within the same turn,
// and we must preserve the first (boundary) event ID.
await dbRun(db, 'UPDATE turns SET event_id = ? WHERE id = ? AND event_id IS NULL', [eventId, turnId]);
});
}
async getTurnEventId(turnId: string): Promise<string | undefined> {
const row = await dbGet(db, 'SELECT event_id FROM turns WHERE id = ?', [turnId]);
return row?.event_id as string | undefined ?? undefined;
}
async getNextTurnEventId(turnId: string): Promise<string | undefined> {
// `turns.id` is the canonical turn key — either a live `request_xxx`
// dispatched by the client or, for sessions restored from disk, the
// SDK envelope id surfaced by `mapSessionEvents`. The `event_id`
// fallback covers the case where the caller asks about a turn that
// was set up live (id=`request_xxx`) but is now being referenced
// via the SDK event id, or vice versa.
const row = await dbGet(
db,
`SELECT event_id FROM turns
WHERE rowid > (
SELECT rowid FROM turns WHERE id = ?1 OR event_id = ?1 LIMIT 1
)
ORDER BY rowid LIMIT 1`,
[turnId],
);
return row?.event_id as string | undefined ?? undefined;
}
async getFirstTurnEventId(): Promise<string | undefined> {
const db = await this._ensureDb();
const row = await dbGet(db, 'SELECT event_id FROM turns ORDER BY rowid LIMIT 1', []);
return row?.event_id as string | undefined ?? undefined;
}
setTurnCheckpointRef(turnId: string, ref: string): Promise<void> {
const db = await this._ensureDb();
await dbRun(db, 'INSERT OR IGNORE INTO turns (id) VALUES (?)', [turnId]);
await dbRun(db, 'UPDATE turns SET checkpoint_ref = ? WHERE id = ?', [ref, turnId]);
});
}
async getTurnCheckpointRef(turnId: string): Promise<string | undefined> {
const row = await dbGet(db, 'SELECT checkpoint_ref FROM turns WHERE id = ?1 OR event_id = ?1 LIMIT 1', [turnId]);
return row?.checkpoint_ref as string | undefined ?? undefined;
}
async getPreviousCheckpointRef(turnId: string): Promise<string | undefined> {
const row = await dbGet(
db,
`SELECT checkpoint_ref FROM turns
WHERE rowid < (SELECT rowid FROM turns WHERE id = ?1 OR event_id = ?1 LIMIT 1)
AND checkpoint_ref IS NOT NULL
ORDER BY rowid DESC LIMIT 1`,
[turnId],
);
return row?.checkpoint_ref as string | undefined ?? undefined;
}
async getAllCheckpointRefs(): Promise<string[]> {
const db = await this._ensureDb();
const rows = await dbAll(db, 'SELECT checkpoint_ref FROM turns WHERE checkpoint_ref IS NOT NULL ORDER BY rowid', []);
return rows.map(r => r.checkpoint_ref as string);
}
truncateFromTurn(turnId: string): Promise<void> {
return this._track(async () => {
const db = await this._ensureDb();
// Delete the target turn and all turns inserted after it (by rowid order).
// File edits cascade-delete via the foreign key constraint.
await dbRun(db,
`DELETE FROM turns WHERE rowid >= (SELECT rowid FROM turns WHERE id = ?)`,
[turnId],
);
});
}
deleteTurnsAfter(turnId: string): Promise<void> {
return this._track(async () => {
const db = await this._ensureDb();
// Delete all turns inserted after the given turn (by rowid order),
// keeping the given turn itself.
// File edits cascade-delete via the foreign key constraint.
await dbRun(db,
`DELETE FROM turns WHERE rowid > (SELECT rowid FROM turns WHERE id = ?)`,
[turnId],
);
});
}
deleteAllTurns(): Promise<void> {
return this._track(async () => {
const db = await this._ensureDb();
await dbExec(db, 'DELETE FROM turns');
});
}
// ---- Local (host-injected) turns ------------------------------------
insertLocalTurn(record: ILocalTurnRecord): Promise<void> {
const db = await this._ensureDb();
await dbRun(db,
'INSERT OR REPLACE INTO local_turns (turn_id, chat_uri, anchor_turn_id, seq, payload) VALUES (?, ?, ?, ?, ?)',
[record.turnId, record.chatUri, record.anchorTurnId ?? null, record.seq, record.payload],
);
});
}
async getLocalTurns(): Promise<ILocalTurnRecord[]> {
const rows = await dbAll(db, 'SELECT turn_id, chat_uri, anchor_turn_id, seq, payload FROM local_turns ORDER BY seq', []);
return rows.map(r => ({
chatUri: r.chat_uri as string,
anchorTurnId: (r.anchor_turn_id as string | null) ?? undefined,
seq: r.seq as number,
payload: r.payload as string,
}
deleteLocalTurns(turnIds: readonly string[]): Promise<void> {
return this._track(async () => {
if (turnIds.length === 0) {
return;
}
const db = await this._ensureDb();
const placeholders = turnIds.map(() => '?').join(',');
await dbRun(db, `DELETE FROM local_turns WHERE turn_id IN (${placeholders})`, [...turnIds]);
});
}
// ---- File edits -----------------------------------------------------
storeFileEdit(edit: IFileEditRecord & IFileEditContent): Promise<void> {
return this._track(() => this._fileEditSequencer.queue(edit.filePath, async () => {
sessionDatabase.ts ×1
const db = await this._ensureDb();
// Ensure the turn exists — lazily insert since the turn record
// may not have been created by an explicit createTurn() call.
await dbRun(db, 'INSERT OR IGNORE INTO turns (id) VALUES (?)', [edit.turnId]);
await dbRun(
db,
`INSERT OR REPLACE INTO file_edits
(turn_id, tool_call_id, file_path, edit_type, original_path, before_content, after_content, added_lines, removed_lines)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
edit.turnId,
edit.toolCallId,
edit.filePath,
edit.kind,
edit.originalPath ?? null,
edit.beforeContent ? Buffer.from(edit.beforeContent) : null,
edit.afterContent ? Buffer.from(edit.afterContent) : null,
edit.addedLines ?? null,
edit.removedLines ?? null,
],
);
}));
}
async getFileEdits(toolCallIds: string[]): Promise<IFileEditRecord[]> {
}
const placeholders = toolCallIds.map(() => '?').join(',');
const rows = await dbAll(
db,
`SELECT turn_id, tool_call_id, file_path, edit_type, original_path, added_lines, removed_lines
FROM file_edits
WHERE tool_call_id IN (${placeholders})
ORDER BY rowid`,
toolCallIds,
);
return rows.map(row => ({
toolCallId: row.tool_call_id as string,
filePath: row.file_path as string,
kind: (row.edit_type as IFileEditRecord['kind']) ?? 'edit',
originalPath: row.original_path as string | undefined ?? undefined,
addedLines: row.added_lines as number | undefined ?? undefined,
removedLines: row.removed_lines as number | undefined ?? undefined,
async getAllFileEdits(): Promise<IFileEditRecord[]> {
const rows = await dbAll(
db,
`SELECT turn_id, tool_call_id, file_path, edit_type, original_path, added_lines, removed_lines
FROM file_edits
ORDER BY rowid`,
[],
);
return rows.map(row => ({
turnId: row.turn_id as string,
toolCallId: row.tool_call_id as string,
filePath: row.file_path as string,
kind: (row.edit_type as IFileEditRecord['kind']) ?? 'edit',
originalPath: row.original_path as string | undefined ?? undefined,
addedLines: row.added_lines as number | undefined ?? undefined,
removedLines: row.removed_lines as number | undefined ?? undefined,
}));
}
async getFileEditsByTurn(turnId: string): Promise<IFileEditRecord[]> {
const db = await this._ensureDb();
const rows = await dbAll(
db,
`SELECT turn_id, tool_call_id, file_path, edit_type, original_path, added_lines, removed_lines
FROM file_edits
WHERE turn_id = ?
ORDER BY rowid`,
[turnId],
);
return rows.map(row => ({
turnId: row.turn_id as string,
toolCallId: row.tool_call_id as string,
filePath: row.file_path as string,
kind: (row.edit_type as IFileEditRecord['kind']) ?? 'edit',
originalPath: row.original_path as string | undefined ?? undefined,
addedLines: row.added_lines as number | undefined ?? undefined,
removedLines: row.removed_lines as number | undefined ?? undefined,
}));
}
async readFileEditContent(toolCallId: string, filePath: string): Promise<IFileEditContent | undefined> {
const db = await this._ensureDb();
const row = await dbGet(
db,
`SELECT before_content, after_content
FROM file_edits
WHERE tool_call_id = ? AND file_path = ?`,
[toolCallId, filePath],
);
if (!row) {
}
beforeContent: row.before_content ? toUint8Array(row.before_content) : undefined,
sessionDatabase.ts ×2
afterContent: row.after_content ? toUint8Array(row.after_content) : undefined,
};
});
}
// ---- Session metadata -----------------------------------------------
async getMetadata(key: string): Promise<string | undefined> {
const row = await dbGet(db, 'SELECT value FROM session_metadata WHERE key = ?', [key]);
return row?.value as string | undefined;
}
async getMetadataObject<T extends Record<string, unknown>>(obj: T): Promise<{ [K in keyof T]: string | undefined }> {
// eslint-disable-next-line local/code-no-dangerous-type-assertions
const result = {} as { [K in keyof T]: string | undefined };
if (keys.length === 0) {
return result;
}
const placeholders = keys.map(() => '?').join(',');
const rows = await dbAll(db, `SELECT key, value FROM session_metadata WHERE key IN (${placeholders})`, keys);
for (const key of keys) {
result[key] = undefined;
}
for (const row of rows) {
}
}
setMetadata(key: string, value: string): Promise<void> {
return this._track(() => this._metadataSequencer.queue(key, async () => {
sessionDatabase.ts ×1
const db = await this._ensureDb();
await dbRun(db, 'INSERT OR REPLACE INTO session_metadata (key, value) VALUES (?, ?)', [key, value]);
}));
}
setChatDraft(chat: URI, draft: Message | undefined): Promise<void> {
return this._track(async () => {
const db = await this._ensureDb();
if (!draft) {
await dbRun(db, 'DELETE FROM chat_drafts WHERE chat_uri = ?', [chatUri]);
sessionDatabase.ts ×1
return;
}
await dbRun(db, 'INSERT OR REPLACE INTO chat_drafts (chat_uri, draft) VALUES (?, ?)', [chatUri, JSON.stringify(draft)]);
sessionDatabase.ts ×2
});
}
async getChatDraft(chat: URI): Promise<Message | undefined> {
const row = await dbGet(db, 'SELECT draft FROM chat_drafts WHERE chat_uri = ?', [chat.toString()]);
if (typeof row?.draft !== 'string') {
}
return JSON.parse(row.draft) as Message;
} catch {
}
// ---- Reviewed files -------------------------------------------------
markFileReviewed(uri: URI, nonce: string): Promise<void> {
const db = await this._ensureDb();
await dbRun(db, 'INSERT OR IGNORE INTO reviewed_files (uri, nonce) VALUES (?, ?)', [uri.toString(), nonce]);
});
}
unmarkFileReviewed(uri: URI, nonce: string): Promise<void> {
const db = await this._ensureDb();
await dbRun(db, 'DELETE FROM reviewed_files WHERE uri = ? AND nonce = ?', [uri.toString(), nonce]);
});
}
async getReviewedFiles(): Promise<IReviewedFileRecord[]> {
const rows = await dbAll(db, 'SELECT uri, nonce FROM reviewed_files ORDER BY rowid', []);
return rows.map(toReviewedFileRecord);
}
async getReviewedFilesForUri(uri: URI): Promise<IReviewedFileRecord[]> {
const rows = await dbAll(db, 'SELECT uri, nonce FROM reviewed_files WHERE uri = ? ORDER BY rowid', [uri.toString()]);
return rows.map(toReviewedFileRecord);
}
async isFileReviewed(uri: URI, nonce: string): Promise<boolean> {
const row = await dbGet(db, 'SELECT 1 FROM reviewed_files WHERE uri = ? AND nonce = ? LIMIT 1', [uri.toString(), nonce]);
return !!row;
}
remapTurnIds(mapping: ReadonlyMap<string, string>): Promise<void> {
return this._track(async () => {
const db = await this._ensureDb();
// Defer FK checks to commit time so we can update turns.id and
// file_edits.turn_id in any order without mid-statement violations.
// This pragma auto-resets after the transaction ends.
await dbExec(db, 'PRAGMA defer_foreign_keys = ON');
await dbExec(db, 'BEGIN TRANSACTION');
try {
// Delete turns not present in the mapping (e.g. turns beyond
// the fork point). File edits cascade-delete via FK.
const oldIds = [...mapping.keys()];
if (oldIds.length > 0) {
const placeholders = oldIds.map(() => '?').join(',');
await dbRun(db,
`DELETE FROM turns WHERE id NOT IN (${placeholders})`,
oldIds,
);
}
// Remap the remaining turn IDs to their new values
for (const [oldId, newId] of mapping) {
await dbRun(db, 'UPDATE turns SET id = ? WHERE id = ?', [newId, oldId]);
await dbRun(db, 'UPDATE file_edits SET turn_id = ? WHERE turn_id = ?', [newId, oldId]);
}
if (oldIds.length > 0) {
const placeholders = oldIds.map(() => '?').join(',');
await dbRun(db,
`DELETE FROM local_turns WHERE turn_id NOT IN (${placeholders})`,
oldIds,
);
}
for (const [oldId, newId] of mapping) {
await dbRun(db, 'UPDATE local_turns SET turn_id = ? WHERE turn_id = ?', [newId, oldId]);
await dbRun(db, 'UPDATE local_turns SET anchor_turn_id = ? WHERE anchor_turn_id = ?', [newId, oldId]);
}
await dbExec(db, 'COMMIT');
} catch (err) {
await dbExec(db, 'ROLLBACK');
throw err;
}
});
}
/**
* Resolves once all currently in-flight write operations have settled.
* Used by graceful shutdown to flush pending fire-and-forget writes
* before the process exits. Should be called from a path where no
* further writes are expected; loops until idle to also drain any
* writes that get queued while we're awaiting.
*/
async whenIdle(): Promise<void> {
while (this._pendingWrites.size > 0) {
await Promise.allSettled([...this._pendingWrites]);
}
}
async vacuumInto(targetPath: string) {
await dbRun(db, 'VACUUM INTO ?', [targetPath]);
}
/**
* Wrap a mutating operation's promise so {@link whenIdle} can await it.
* Invoke at the **outermost** layer of every public mutating method so
* that any internal awaits (notably `_ensureDb()`) are covered too —
* tracking only the leaf `dbRun`/`dbExec` would miss the window
* between the method being called and the query actually being queued.
*/
private _track<T>(fn: () => Promise<T>): Promise<T> {
this._pendingWrites.add(p);
const untrack = () => { this._pendingWrites.delete(p); };
p.then(untrack, untrack);
return p;
}
async close() {
await (this._closed ??= this._dbPromise?.then(db => dbClose(db)).catch(() => { }) || true);
sessionDatabase.ts ×2
}
dispose(): void {
}
function toReviewedFileRecord(row: Record<string, unknown>): IReviewedFileRecord {
sessionDatabase.ts ×1
return {
uri: URI.parse(row.uri as string),
nonce: row.nonce as string,
};
}
if (value instanceof Buffer) {
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
}
if (value instanceof Uint8Array) {
return value;
}
if (typeof value === 'string') {
return new TextEncoder().encode(value);
}
return new Uint8Array(0);
}