History

Add Effect persistence runtime modules and wire service bootstrap

Cursor Agent committed 8 months ago

Commit 2cf729e1 parent 40d49f110 files · +80 −21Browse files

Unified · Split

10 files
  1. apps/server/src/persistence/config.ts +16 −0
  2. apps/server/src/persistence/errors.ts +6 −0
  3. apps/server/src/persistence/migrator.ts +6 −0
  4. apps/server/src/persistence/repos/documentsRepo.ts +1 −0
  5. apps/server/src/persistence/repos/metadataRepo.ts +1 −0
  6. apps/server/src/persistence/repos/providerEventsRepo.ts +1 −0
  7. apps/server/src/persistence/repos/stateEventsRepo.ts +1 −0
  8. apps/server/src/persistence/runtime.ts +19 −0
  9. apps/server/src/persistence/sqliteLayer.ts +10 −0
  10. apps/server/src/persistenceService.ts +19 −21
apps/server/src/persistence/config.ts+16−0
@@ -0,0 +1,16 @@
1import path from "node:path";
2
3export interface PersistenceConfig {
4 dbPath: string;
5 legacyProjectsJsonPath?: string;
6}
7
8export function resolvePersistenceConfig(input: PersistenceConfig): PersistenceConfig {
9 const resolved: PersistenceConfig = {
10 dbPath: path.resolve(input.dbPath),
11 };
12 if (input.legacyProjectsJsonPath) {
13 resolved.legacyProjectsJsonPath = path.resolve(input.legacyProjectsJsonPath);
14 }
15 return resolved;
16}
apps/server/src/persistence/errors.ts+6−0
@@ -0,0 +1,6 @@
1export class PersistenceInitializationError extends Error {
2 constructor(message: string, options?: { cause?: unknown }) {
3 super(message, options);
4 this.name = "PersistenceInitializationError";
5 }
6}
apps/server/src/persistence/migrator.ts+6−0
@@ -0,0 +1,6 @@
1import type { SqliteDatabase } from "../sqliteAdapter";
2import { runStateMigrations } from "../stateMigrations";
3
4export function runPersistenceMigrations(db: SqliteDatabase): void {
5 runStateMigrations(db);
6}
apps/server/src/persistence/repos/documentsRepo.ts+1−0
@@ -0,0 +1 @@
1export * from "../documentsRepo";
apps/server/src/persistence/repos/metadataRepo.ts+1−0
@@ -0,0 +1 @@
1export * from "../metadataRepo";
apps/server/src/persistence/repos/providerEventsRepo.ts+1−0
@@ -0,0 +1 @@
1export * from "../providerEventsRepo";
apps/server/src/persistence/repos/stateEventsRepo.ts+1−0
@@ -0,0 +1 @@
1export * from "../stateEventsRepo";
apps/server/src/persistence/runtime.ts+19−0
@@ -0,0 +1,19 @@
1import type * as Effect from "effect/Effect";
2import type * as SqlClient from "effect/unstable/sql/SqlClient";
3
4import type { EffectSqliteDatabaseAdapter, SqliteDatabase } from "../sqliteAdapter";
5
6function isEffectSqliteDatabase(db: SqliteDatabase): db is EffectSqliteDatabaseAdapter {
7 return "runWithSqlClient" in db && typeof db.runWithSqlClient === "function";
8}
9
10export function runWithSqlClient<A>(
11 db: SqliteDatabase,
12 effect: Effect.Effect<A, unknown, SqlClient.SqlClient>,
13 fallback: () => A,
14): A {
15 if (isEffectSqliteDatabase(db)) {
16 return db.runWithSqlClient(effect);
17 }
18 return fallback();
19}
apps/server/src/persistence/sqliteLayer.ts+10−0
@@ -0,0 +1,10 @@
1import fs from "node:fs";
2import path from "node:path";
3
4import { openSqliteDatabase, type SqliteDatabase } from "../sqliteAdapter";
5
6export function openPersistenceSqliteDatabase(dbPath: string): SqliteDatabase {
7 const resolvedPath = path.resolve(dbPath);
8 fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
9 return openSqliteDatabase(resolvedPath);
10}
apps/server/src/persistenceService.ts+19−21
@@ -77,23 +77,23 @@ import { · 76 unmodified lines
77 readNextSortKey as readNextSortKeyEffect,77 readNextSortKey as readNextSortKeyEffect,
78 upsertDocument as upsertDocumentEffect,78 upsertDocument as upsertDocumentEffect,
79 type DocumentRow,79 type DocumentRow,
80} from "./persistence/documentsRepo";80} from "./persistence/repos/documentsRepo";
81import {81import {
82 insertProviderEvent as insertProviderEventEffect,82 insertProviderEvent as insertProviderEventEffect,
83 listCompletedItemEventsBySessionTurn as listCompletedItemEventsBySessionTurnEffect,83 listCompletedItemEventsBySessionTurn as listCompletedItemEventsBySessionTurnEffect,
84 listCompletedItemEventsByThreadTurn as listCompletedItemEventsByThreadTurnEffect,84 listCompletedItemEventsByThreadTurn as listCompletedItemEventsByThreadTurnEffect,
85 listCompletedItemEventsByTurn as listCompletedItemEventsByTurnEffect,85 listCompletedItemEventsByTurn as listCompletedItemEventsByTurnEffect,
86 type CompletedProviderItemRow,86 type CompletedProviderItemRow,
87} from "./persistence/providerEventsRepo";87} from "./persistence/repos/providerEventsRepo";
88import {88import {
89 appendStateEvent as appendStateEventEffect,89 appendStateEvent as appendStateEventEffect,
90 listStateEventsAfterSeq as listStateEventsAfterSeqEffect,90 listStateEventsAfterSeq as listStateEventsAfterSeqEffect,
91 readLastStateSeq as readLastStateSeqEffect,91 readLastStateSeq as readLastStateSeqEffect,
92} from "./persistence/stateEventsRepo";92} from "./persistence/repos/stateEventsRepo";
93import {93import {
94 readMetadataValue as readMetadataValueEffect,94 readMetadataValue as readMetadataValueEffect,
95 writeMetadataValue as writeMetadataValueEffect,95 writeMetadataValue as writeMetadataValueEffect,
96} from "./persistence/metadataRepo";96} from "./persistence/repos/metadataRepo";
97import {97import {
98 buildUpdatedAppSettings,98 buildUpdatedAppSettings,
99 resolveAppSettings,99 resolveAppSettings,
@@ -119,8 +119,12 @@ import { · 19 unmodified lines
119} from "./persistence/domain/stateSync";119} from "./persistence/domain/stateSync";
120import { fallbackGroupId, normalizeTerminalIds, normalizeThread } from "./persistence/domain/threads";120import { fallbackGroupId, normalizeTerminalIds, normalizeThread } from "./persistence/domain/threads";
121import { mergeTurnSummaryFiles, summarizeUnifiedDiff } from "./persistence/domain/turnSummaries";121import { mergeTurnSummaryFiles, summarizeUnifiedDiff } from "./persistence/domain/turnSummaries";
122import { openSqliteDatabase, type EffectSqliteDatabaseAdapter, type SqliteDatabase } from "./sqliteAdapter";122import { resolvePersistenceConfig } from "./persistence/config";
123import { runStateMigrations } from "./stateMigrations";123import { PersistenceInitializationError } from "./persistence/errors";
124import { runPersistenceMigrations } from "./persistence/migrator";
125import { runWithSqlClient } from "./persistence/runtime";
126import { openPersistenceSqliteDatabase } from "./persistence/sqliteLayer";
127import type { SqliteDatabase } from "./sqliteAdapter";
124 128
125const METADATA_KEY_PROJECTS_JSON_IMPORTED = "migration.projects_json_imported";129const METADATA_KEY_PROJECTS_JSON_IMPORTED = "migration.projects_json_imported";
126const METADATA_KEY_APP_SETTINGS = "app.settings.v1";130const METADATA_KEY_APP_SETTINGS = "app.settings.v1";
@@ -197,21 +201,22 @@ export class PersistenceService extends EventEmitter<PersistenceServiceEvents> { · 70 unmodified lines
197 201
198 constructor(options: PersistenceServiceOptions) {202 constructor(options: PersistenceServiceOptions) {
199 super();203 super();
200 const dbPath = path.resolve(options.dbPath);204 const config = resolvePersistenceConfig(options);
201 fs.mkdirSync(path.dirname(dbPath), { recursive: true });205 this.db = openPersistenceSqliteDatabase(config.dbPath);
202 this.db = openSqliteDatabase(dbPath);
203 try {206 try {
204 runStateMigrations(this.db);207 runPersistenceMigrations(this.db);
205 } catch (error) {208 } catch (error) {
206 try {209 try {
207 this.db.close();210 this.db.close();
208 } catch {211 } catch {
209 // Best effort close on failed initialization.212 // Best effort close on failed initialization.
210 }213 }
211 throw error;214 throw new PersistenceInitializationError("Failed to initialize persistence database", {
215 cause: error,
216 });
212 }217 }
213 if (options.legacyProjectsJsonPath) {218 if (config.legacyProjectsJsonPath) {
214 this.importProjectsJsonIfNeeded(options.legacyProjectsJsonPath);219 this.importProjectsJsonIfNeeded(config.legacyProjectsJsonPath);
215 }220 }
216 }221 }
217 222
@@ -1716,13 +1721,6 @@ export class PersistenceService extends EventEmitter<PersistenceServiceEvents> { · 1498 unmodified lines
1716 effect: Effect.Effect<A, unknown, SqlClient.SqlClient>,1721 effect: Effect.Effect<A, unknown, SqlClient.SqlClient>,
1717 fallback: () => A,1722 fallback: () => A,
1718 ): A {1723 ): A {
1719 if (isEffectSqliteDatabase(this.db)) {1724 return runWithSqlClient(this.db, effect, fallback);
1720 return this.db.runWithSqlClient(effect);
1721 }
1722 return fallback();
1723 }1725 }
1724}1726}
1725
1726function isEffectSqliteDatabase(db: SqliteDatabase): db is EffectSqliteDatabaseAdapter {
1727 return "runWithSqlClient" in db && typeof db.runWithSqlClient === "function";
1728}