Unified · Split
| @@ -66,7 +66,6 @@ import { · 65 unmodified lines | |||
| 66 | threadsUpdateResultSchema, | 66 | threadsUpdateResultSchema, |
| 67 | } from "@t3tools/contracts"; | 67 | } from "@t3tools/contracts"; |
| 68 | 68 | ||
| 69 | import { StateDb } from "./stateDb"; | ||
| 70 | import { | 69 | import { |
| 71 | countMessagesForThread as countMessagesForThreadEffect, | 70 | countMessagesForThread as countMessagesForThreadEffect, |
| 72 | deleteDocumentByIdAndKind as deleteDocumentByIdAndKindEffect, | 71 | deleteDocumentByIdAndKind as deleteDocumentByIdAndKindEffect, |
| @@ -101,7 +100,8 @@ import { · 28 unmodified lines | |||
| 101 | readMetadataValue as readMetadataValueEffect, | 100 | readMetadataValue as readMetadataValueEffect, |
| 102 | writeMetadataValue as writeMetadataValueEffect, | 101 | writeMetadataValue as writeMetadataValueEffect, |
| 103 | } from "./persistence/metadataRepo"; | 102 | } from "./persistence/metadataRepo"; |
| 104 | import type { EffectSqliteDatabaseAdapter } from "./sqliteAdapter"; | 103 | import { openSqliteDatabase, type EffectSqliteDatabaseAdapter, type SqliteDatabase } from "./sqliteAdapter"; |
| 104 | import { runStateMigrations } from "./stateMigrations"; | ||
| 105 | 105 | ||
| 106 | const METADATA_KEY_PROJECTS_JSON_IMPORTED = "migration.projects_json_imported"; | 106 | const METADATA_KEY_PROJECTS_JSON_IMPORTED = "migration.projects_json_imported"; |
| 107 | const METADATA_KEY_APP_SETTINGS = "app.settings.v1"; | 107 | const METADATA_KEY_APP_SETTINGS = "app.settings.v1"; |
| @@ -466,22 +466,32 @@ function mergeTurnSummaryFiles( · 358 unmodified lines | |||
| 466 | } | 466 | } |
| 467 | 467 | ||
| 468 | export class PersistenceService extends EventEmitter<PersistenceServiceEvents> { | 468 | export class PersistenceService extends EventEmitter<PersistenceServiceEvents> { |
| 469 | private readonly stateDb: StateDb; | 469 | private readonly db: SqliteDatabase; |
| 470 | private readonly db: StateDb["db"]; | ||
| 471 | private readonly sessionThreadIds = new Map<string, string>(); | 470 | private readonly sessionThreadIds = new Map<string, string>(); |
| 472 | private readonly runtimeThreadIds = new Map<string, string>(); | 471 | private readonly runtimeThreadIds = new Map<string, string>(); |
| 473 | 472 | ||
| 474 | constructor(options: PersistenceServiceOptions) { | 473 | constructor(options: PersistenceServiceOptions) { |
| 475 | super(); | 474 | super(); |
| 476 | this.stateDb = new StateDb({ dbPath: options.dbPath }); | 475 | const dbPath = path.resolve(options.dbPath); |
| 477 | this.db = this.stateDb.db; | 476 | fs.mkdirSync(path.dirname(dbPath), { recursive: true }); |
| 477 | this.db = openSqliteDatabase(dbPath); | ||
| 478 | try { | ||
| 479 | runStateMigrations(this.db); | ||
| 480 | } catch (error) { | ||
| 481 | try { | ||
| 482 | this.db.close(); | ||
| 483 | } catch { | ||
| 484 | // Best effort close on failed initialization. | ||
| 485 | } | ||
| 486 | throw error; | ||
| 487 | } | ||
| 478 | if (options.legacyProjectsJsonPath) { | 488 | if (options.legacyProjectsJsonPath) { |
| 479 | this.importProjectsJsonIfNeeded(options.legacyProjectsJsonPath); | 489 | this.importProjectsJsonIfNeeded(options.legacyProjectsJsonPath); |
| 480 | } | 490 | } |
| 481 | } | 491 | } |
| 482 | 492 | ||
| 483 | close(): void { | 493 | close(): void { |
| 484 | this.stateDb.close(); | 494 | this.db.close(); |
| 485 | } | 495 | } |
| 486 | 496 | ||
| 487 | getAppSettings(): AppSettings { | 497 | getAppSettings(): AppSettings { |
| @@ -1379,7 +1389,7 @@ export class PersistenceService extends EventEmitter<PersistenceServiceEvents> { · 891 unmodified lines | |||
| 1379 | } | 1389 | } |
| 1380 | 1390 | ||
| 1381 | let importedCount = 0; | 1391 | let importedCount = 0; |
| 1382 | this.stateDb.transaction(() => { | 1392 | this.runDbTransaction(() => { |
| 1383 | try { | 1393 | try { |
| 1384 | const raw = fs.readFileSync(normalizedPath, "utf8"); | 1394 | const raw = fs.readFileSync(normalizedPath, "utf8"); |
| 1385 | const payload = JSON.parse(raw) as { projects?: unknown }; | 1395 | const payload = JSON.parse(raw) as { projects?: unknown }; |
| @@ -1431,9 +1441,25 @@ export class PersistenceService extends EventEmitter<PersistenceServiceEvents> { · 45 unmodified lines | |||
| 1431 | }); | 1441 | }); |
| 1432 | } | 1442 | } |
| 1433 | 1443 | ||
| 1444 | private runDbTransaction<T>(fn: () => T): T { | ||
| 1445 | this.db.exec("BEGIN IMMEDIATE;"); | ||
| 1446 | try { | ||
| 1447 | const result = fn(); | ||
| 1448 | this.db.exec("COMMIT;"); | ||
| 1449 | return result; | ||
| 1450 | } catch (error) { | ||
| 1451 | try { | ||
| 1452 | this.db.exec("ROLLBACK;"); | ||
| 1453 | } catch { | ||
| 1454 | // Preserve the original transactional failure. | ||
| 1455 | } | ||
| 1456 | throw error; | ||
| 1457 | } | ||
| 1458 | } | ||
| 1459 | |||
| 1434 | private withTransaction<T>(fn: (pendingEvents: StateEvent[]) => T): T { | 1460 | private withTransaction<T>(fn: (pendingEvents: StateEvent[]) => T): T { |
| 1435 | const pendingEvents: StateEvent[] = []; | 1461 | const pendingEvents: StateEvent[] = []; |
| 1436 | const result = this.stateDb.transaction(() => fn(pendingEvents)); | 1462 | const result = this.runDbTransaction(() => fn(pendingEvents)); |
| 1437 | for (const event of pendingEvents) { | 1463 | for (const event of pendingEvents) { |
| 1438 | try { | 1464 | try { |
| 1439 | this.emit("stateEvent", event); | 1465 | this.emit("stateEvent", event); |
| @@ -1983,7 +2009,7 @@ export class PersistenceService extends EventEmitter<PersistenceServiceEvents> { · 543 unmodified lines | |||
| 1983 | write(); | 2009 | write(); |
| 1984 | return; | 2010 | return; |
| 1985 | } | 2011 | } |
| 1986 | this.stateDb.transaction(write); | 2012 | this.runDbTransaction(write); |
| 1987 | } | 2013 | } |
| 1988 | 2014 | ||
| 1989 | private runWithEffectSql<A>( | 2015 | private runWithEffectSql<A>( |
| @@ -1997,6 +2023,6 @@ export class PersistenceService extends EventEmitter<PersistenceServiceEvents> { · 7 unmodified lines | |||
| 1997 | } | 2023 | } |
| 1998 | } | 2024 | } |
| 1999 | 2025 | ||
| 2000 | function isEffectSqliteDatabase(db: StateDb["db"]): db is EffectSqliteDatabaseAdapter { | 2026 | function isEffectSqliteDatabase(db: SqliteDatabase): db is EffectSqliteDatabaseAdapter { |
| 2001 | return "runWithSqlClient" in db && typeof db.runWithSqlClient === "function"; | 2027 | return "runWithSqlClient" in db && typeof db.runWithSqlClient === "function"; |
| 2002 | } | 2028 | } |