diff --git a/apps/server/package.json b/apps/server/package.json index cdd7b268c35d972416e46124998f74c6c9510305..d92adb10ee6737b9b412b57d9f15127a5ec3d3c7 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -17,6 +17,8 @@ "test": "vitest run" }, "dependencies": { + "@effect/platform": "^0.94.5", + "@effect/platform-node": "^0.104.1", "@effect/sql": "^0.49.0", "@effect/sql-sqlite-node": "^0.50.1", "effect": "^3.19.19", diff --git a/apps/server/src/orchestration/engine.test.ts b/apps/server/src/orchestration/engine.test.ts index 59789bfa744c9d1071753c543133fc2cc8f32ab8..666dff3d5bbeb52214e6243243eb4788d930aaf7 100644 --- a/apps/server/src/orchestration/engine.test.ts +++ b/apps/server/src/orchestration/engine.test.ts @@ -8,6 +8,7 @@ import { afterEach, describe, expect, it } from "vitest"; import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents"; import { OrchestrationEngine } from "./engine"; +import { createOrchestrationSystem } from "./runtime"; const tempDirs: string[] = []; @@ -30,8 +31,8 @@ describe("OrchestrationEngine", () => { const projectId = "project-1"; const threadId = "thread-1"; - const engineA = new OrchestrationEngine(stateDir); - await engineA.start(); + const firstSystem = await createOrchestrationSystem(stateDir); + const engineA = firstSystem.engine; await engineA.dispatch({ type: "project.create", commandId: "cmd-1", @@ -63,19 +64,19 @@ describe("OrchestrationEngine", () => { createdAt, }); const snapshotA = engineA.getSnapshot(); - await engineA.stop(); + await firstSystem.dispose(); - const engineB = new OrchestrationEngine(stateDir); - await engineB.start(); + const secondSystem = await createOrchestrationSystem(stateDir); + const engineB = secondSystem.engine; const snapshotB = engineB.getSnapshot(); expect(snapshotB).toEqual(snapshotA); - await engineB.stop(); + await secondSystem.dispose(); }); it("fans out read-model updates to subscribers", async () => { const stateDir = makeTempDir("t3code-orchestration-fanout-"); - const engine = new OrchestrationEngine(stateDir); - await engine.start(); + const system = await createOrchestrationSystem(stateDir); + const engine = system.engine; const updates: number[] = []; const unsubscribe = engine.subscribeToReadModel((snapshot) => { updates.push(snapshot.sequence); @@ -91,13 +92,13 @@ describe("OrchestrationEngine", () => { }); unsubscribe(); expect(updates.length).toBeGreaterThan(0); - await engine.stop(); + await system.dispose(); }); it("replays append-only events from sequence", async () => { const stateDir = makeTempDir("t3code-orchestration-replay-"); - const engine = new OrchestrationEngine(stateDir); - await engine.start(); + const system = await createOrchestrationSystem(stateDir); + const engine = system.engine; await engine.dispatch({ type: "project.create", commandId: "cmd-a", @@ -117,14 +118,14 @@ describe("OrchestrationEngine", () => { expect(events.length).toBe(2); expect(events[0]?.type).toBe("project.created"); expect(events[1]?.type).toBe("project.deleted"); - await engine.stop(); + await system.dispose(); }); it("stores completed turn summaries even when no files changed", async () => { const stateDir = makeTempDir("t3code-orchestration-turn-diff-"); - const engine = new OrchestrationEngine(stateDir); + const firstSystem = await createOrchestrationSystem(stateDir); + const engine = firstSystem.engine; const completedAt = new Date().toISOString(); - await engine.start(); await engine.dispatch({ type: "project.create", commandId: "cmd-project-turn-diff", @@ -167,10 +168,10 @@ describe("OrchestrationEngine", () => { checkpointTurnCount: 1, }, ]); - await engine.stop(); + await firstSystem.dispose(); - const restartedEngine = new OrchestrationEngine(stateDir); - await restartedEngine.start(); + const secondSystem = await createOrchestrationSystem(stateDir); + const restartedEngine = secondSystem.engine; const restartedThread = restartedEngine .getSnapshot() .threads.find((thread) => thread.id === "thread-turn-diff"); @@ -183,14 +184,14 @@ describe("OrchestrationEngine", () => { checkpointTurnCount: 1, }, ]); - await restartedEngine.stop(); + await secondSystem.dispose(); }); it("reverts thread messages and turn summaries to a checkpoint", async () => { const stateDir = makeTempDir("t3code-orchestration-revert-"); - const engine = new OrchestrationEngine(stateDir); + const firstSystem = await createOrchestrationSystem(stateDir); + const engine = firstSystem.engine; const createdAt = new Date().toISOString(); - await engine.start(); await engine.dispatch({ type: "project.create", commandId: "cmd-project-revert", @@ -291,10 +292,10 @@ describe("OrchestrationEngine", () => { expect(thread?.messages.map((message) => message.id)).toEqual(["user-1", "assistant:turn-1"]); expect(thread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]); expect(thread?.latestTurnId).toBe("turn-1"); - await engine.stop(); + await firstSystem.dispose(); - const restarted = new OrchestrationEngine(stateDir); - await restarted.start(); + const secondSystem = await createOrchestrationSystem(stateDir); + const restarted = secondSystem.engine; const restartedThread = restarted .getSnapshot() .threads.find((entry) => entry.id === "thread-revert"); @@ -303,12 +304,22 @@ describe("OrchestrationEngine", () => { "assistant:turn-1", ]); expect(restartedThread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]); - await restarted.stop(); + await secondSystem.dispose(); }); it("allows stop to be called multiple times", async () => { - const stateDir = makeTempDir("t3code-orchestration-stop-"); - const engine = new OrchestrationEngine(stateDir); + const inMemoryStore: OrchestrationEventRepositoryShape = { + append(event) { + return Effect.succeed({ ...event, sequence: 1 }); + }, + readFromSequence() { + return Effect.succeed([]); + }, + readAll() { + return Effect.succeed([]); + }, + }; + const engine = new OrchestrationEngine(inMemoryStore); await engine.start(); await engine.stop(); await expect(engine.stop()).resolves.toBeUndefined(); @@ -341,7 +352,6 @@ describe("OrchestrationEngine", () => { readAll() { return Effect.succeed(events); }, - close() {}, }; const engine = new OrchestrationEngine(flakyStore); diff --git a/apps/server/src/orchestration/engine.ts b/apps/server/src/orchestration/engine.ts index 56ba03ace3848cf0d2431176ae39e591fad66df9..4069645dd90f416e67954030d3ddd081a1b62b4d 100644 --- a/apps/server/src/orchestration/engine.ts +++ b/apps/server/src/orchestration/engine.ts @@ -1,5 +1,3 @@ -import path from "node:path"; - import type { OrchestrationCommand, OrchestrationEvent, @@ -21,7 +19,6 @@ import { import { createLogger } from "../logger"; import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents"; -import { makeSqliteOrchestrationEventRepository } from "../persistence/Layers/OrchestrationEvents"; import { createEmptyReadModel, reduceEvent } from "./reducer"; import { UI_ENTITY_CONTRACTS } from "./uiContractInventory"; @@ -215,15 +212,8 @@ export class OrchestrationEngine { private readonly readModelListeners = new Set<(snapshot: OrchestrationReadModel) => void>(); private readonly domainEventListeners = new Set<(event: OrchestrationEvent) => void>(); - constructor(stateDir: string); - constructor(eventStore: OrchestrationEventRepositoryShape); - constructor(stateDirOrEventStore: string | OrchestrationEventRepositoryShape) { - if (typeof stateDirOrEventStore === "string") { - const dbPath = path.join(stateDirOrEventStore, "orchestration.sqlite"); - this.eventStore = makeSqliteOrchestrationEventRepository(dbPath); - } else { - this.eventStore = stateDirOrEventStore; - } + constructor(eventStore: OrchestrationEventRepositoryShape) { + this.eventStore = eventStore; this.readModel = createEmptyReadModel(new Date().toISOString()); this.commandQueue = Runtime.runSync(this.runtime)(Queue.unbounded()); this.readModelPubSub = Runtime.runSync(this.runtime)( @@ -279,6 +269,10 @@ export class OrchestrationEngine { } async start(): Promise { + if (this.workerFiber) { + return; + } + await Runtime.runPromise(this.runtime)(this.bootstrapReadModel()); const worker = Stream.fromQueue(this.commandQueue).pipe( @@ -297,7 +291,6 @@ export class OrchestrationEngine { await Runtime.runPromise(this.runtime)(Fiber.interrupt(this.workerFiber)); this.workerFiber = null; } - this.eventStore.close(); } getSnapshot(): OrchestrationReadModel { diff --git a/apps/server/src/orchestration/layers.test.ts b/apps/server/src/orchestration/layers.test.ts index da24a9b7c79c7b7bd6278cbbbe3a9b15015eb203..440af03bbdb0d97dd09141dfc8ca622ca0c5a11c 100644 --- a/apps/server/src/orchestration/layers.test.ts +++ b/apps/server/src/orchestration/layers.test.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { createOrchestrationEngine } from "./runtime"; +import { createOrchestrationSystem } from "./runtime"; const tempDirs: string[] = []; @@ -24,9 +24,9 @@ describe("orchestration layers", () => { it("creates an engine through Context.Tag wiring", async () => { const stateDir = makeTempDir("t3code-orchestration-layer-"); const createdAt = new Date().toISOString(); - const engine = createOrchestrationEngine(stateDir); + const firstSystem = await createOrchestrationSystem(stateDir); + const { engine } = firstSystem; - await engine.start(); await engine.dispatch({ type: "project.create", commandId: "cmd-layer-1", @@ -37,11 +37,11 @@ describe("orchestration layers", () => { createdAt, }); const firstSnapshot = engine.getSnapshot(); - await engine.stop(); + await firstSystem.dispose(); - const restarted = createOrchestrationEngine(stateDir); - await restarted.start(); + const secondSystem = await createOrchestrationSystem(stateDir); + const restarted = secondSystem.engine; expect(restarted.getSnapshot()).toEqual(firstSnapshot); - await restarted.stop(); + await secondSystem.dispose(); }); }); diff --git a/apps/server/src/orchestration/layers.ts b/apps/server/src/orchestration/layers.ts index e2b4d2d1c82dce22d6613b7a56bfcac3d324e6fd..63009c9c9b7bfc24fe6f5ce32debb7545b7a9664 100644 --- a/apps/server/src/orchestration/layers.ts +++ b/apps/server/src/orchestration/layers.ts @@ -3,23 +3,28 @@ import path from "node:path"; import { Effect, Layer } from "effect"; import { OrchestrationEventRepository } from "../persistence/Services/OrchestrationEvents"; -import { makeSqliteOrchestrationEventRepository } from "../persistence/Layers/OrchestrationEvents"; +import { makeSqliteOrchestrationEventRepositoryLive } from "../persistence/Layers/OrchestrationEvents"; import { OrchestrationEngine } from "./engine"; import { OrchestrationConfig, OrchestrationEngineService } from "./services"; -export const OrchestrationEventRepositoryLive = Layer.effect( - OrchestrationEventRepository, +export const OrchestrationEventRepositoryLive = Layer.unwrapEffect( Effect.map(OrchestrationConfig, ({ stateDir }) => { const dbPath = path.join(stateDir, "orchestration.sqlite"); - return makeSqliteOrchestrationEventRepository(dbPath); + return makeSqliteOrchestrationEventRepositoryLive(dbPath); }), ); -export const OrchestrationEngineLive = Layer.effect( +export const OrchestrationEngineLive = Layer.scoped( OrchestrationEngineService, - Effect.map(OrchestrationEventRepository, (eventRepository) => { - return new OrchestrationEngine(eventRepository); - }), + Effect.acquireRelease( + Effect.gen(function* () { + const eventRepository = yield* OrchestrationEventRepository; + const engine = new OrchestrationEngine(eventRepository); + yield* Effect.promise(() => engine.start()); + return engine; + }), + (engine) => Effect.promise(() => engine.stop()), + ), ); export const OrchestrationLive = OrchestrationEngineLive.pipe( diff --git a/apps/server/src/orchestration/runtime.ts b/apps/server/src/orchestration/runtime.ts index cbb875b5f907d9a29a1f51c1500718561ad28f0d..67851be2f5432b2083636d55b4c610b1c7cea941 100644 --- a/apps/server/src/orchestration/runtime.ts +++ b/apps/server/src/orchestration/runtime.ts @@ -1,14 +1,22 @@ -import { Effect, Layer, Runtime } from "effect"; +import { Layer, ManagedRuntime } from "effect"; import type { OrchestrationEngine } from "./engine"; import { OrchestrationLive } from "./layers"; import { OrchestrationConfig, OrchestrationEngineService } from "./services"; -export function createOrchestrationEngine(stateDir: string): OrchestrationEngine { +export interface OrchestrationSystem { + readonly engine: OrchestrationEngine; + readonly dispose: () => Promise; +} + +export async function createOrchestrationSystem(stateDir: string): Promise { const orchestrationLayer = OrchestrationLive.pipe( Layer.provide(Layer.succeed(OrchestrationConfig, { stateDir })), ); - return Runtime.runSync(Runtime.defaultRuntime)( - Effect.provide(OrchestrationEngineService, orchestrationLayer), - ); + const runtime = ManagedRuntime.make(orchestrationLayer); + const engine = await runtime.runPromise(OrchestrationEngineService); + return { + engine, + dispose: () => runtime.dispose(), + }; } diff --git a/apps/server/src/persistence/Layers/OrchestrationEvents.test.ts b/apps/server/src/persistence/Layers/OrchestrationEvents.test.ts index 60ee3a7a4272b3e9a17ad43298009a7471986070..57f75fb55d610bbe2486f22d6fe1450fa0cfa00e 100644 --- a/apps/server/src/persistence/Layers/OrchestrationEvents.test.ts +++ b/apps/server/src/persistence/Layers/OrchestrationEvents.test.ts @@ -3,10 +3,11 @@ import os from "node:os"; import path from "node:path"; import { DatabaseSync } from "node:sqlite"; -import { Runtime } from "effect"; +import { Runtime, ManagedRuntime } from "effect"; import { afterEach, describe, expect, it } from "vitest"; -import { makeSqliteOrchestrationEventRepository } from "./OrchestrationEvents"; +import { OrchestrationEventRepository } from "../Services/OrchestrationEvents"; +import { makeSqliteOrchestrationEventRepositoryLive } from "./OrchestrationEvents"; const tempDirs: string[] = []; @@ -22,14 +23,15 @@ afterEach(() => { } }); -describe("SqliteEventStore", () => { +describe("OrchestrationEventRepository", () => { it("persists and replays events across restarts", async () => { const stateDir = makeTempDir("t3code-event-store-"); const dbPath = path.join(stateDir, "orchestration.sqlite"); const runtime = Runtime.defaultRuntime; const createdAt = new Date().toISOString(); - const first = makeSqliteOrchestrationEventRepository(dbPath); + const firstRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath)); + const first = await firstRuntime.runPromise(OrchestrationEventRepository); const saved = await Runtime.runPromise(runtime)( first.append({ eventId: "event-1", @@ -42,12 +44,13 @@ describe("SqliteEventStore", () => { }), ); expect(saved.sequence).toBe(1); - first.close(); + await firstRuntime.dispose(); - const second = makeSqliteOrchestrationEventRepository(dbPath); + const secondRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath)); + const second = await secondRuntime.runPromise(OrchestrationEventRepository); const replayed = await Runtime.runPromise(runtime)(second.readFromSequence(0)); expect(replayed).toEqual([saved]); - second.close(); + await secondRuntime.dispose(); }); it("creates and reuses the migrator tracking table", async () => { @@ -55,13 +58,15 @@ describe("SqliteEventStore", () => { const dbPath = path.join(stateDir, "orchestration.sqlite"); const runtime = Runtime.defaultRuntime; - const first = makeSqliteOrchestrationEventRepository(dbPath); + const firstRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath)); + const first = await firstRuntime.runPromise(OrchestrationEventRepository); await Runtime.runPromise(runtime)(first.readAll()); - first.close(); + await firstRuntime.dispose(); - const second = makeSqliteOrchestrationEventRepository(dbPath); + const secondRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath)); + const second = await secondRuntime.runPromise(OrchestrationEventRepository); await Runtime.runPromise(runtime)(second.readAll()); - second.close(); + await secondRuntime.dispose(); const db = new DatabaseSync(dbPath, { readOnly: true }); const tableRow = db diff --git a/apps/server/src/persistence/Layers/OrchestrationEvents.ts b/apps/server/src/persistence/Layers/OrchestrationEvents.ts index bafdcd2a434c0978c28b1dd95e8ad28d284a2b44..56bdf08bdb8a5799e684a5406c2b9627dcb66107 100644 --- a/apps/server/src/persistence/Layers/OrchestrationEvents.ts +++ b/apps/server/src/persistence/Layers/OrchestrationEvents.ts @@ -7,10 +7,13 @@ import type { SqlClient as SqlClientService } from "@effect/sql/SqlClient"; import * as SqlClient from "@effect/sql/SqlClient"; import * as SqlSchema from "@effect/sql/SqlSchema"; import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient"; -import { Effect, ManagedRuntime, Schema } from "effect"; +import { Context, Effect, Layer, Schema } from "effect"; -import type { OrchestrationEventRepositoryShape } from "../Services/OrchestrationEvents"; import { runMigrations } from "../Migrations"; +import { + OrchestrationEventRepository, + type OrchestrationEventRepositoryShape, +} from "../Services/OrchestrationEvents"; const decodeEvent = Schema.decodeUnknownSync(OrchestrationEventSchema); @@ -42,11 +45,31 @@ const ReadFromSequenceRequestSchema = Schema.Struct({ limit: Schema.Number, }); -const appendEventRow = SqlSchema.single({ - Request: AppendEventRequestSchema, - Result: EventRowSchema, - execute: (request) => - Effect.flatMap(SqlClient.SqlClient, (sql) => +class OrchestrationSql extends Context.Tag("persistence/OrchestrationSql")< + OrchestrationSql, + SqlClientService +>() {} + +function eventRowToOrchestrationEvent(row: EventRow): OrchestrationEvent { + return decodeEvent({ + sequence: row.sequence, + eventId: row.eventId, + type: row.type, + aggregateType: row.aggregateType, + aggregateId: row.aggregateId, + occurredAt: row.occurredAt, + commandId: row.commandId, + payload: JSON.parse(row.payloadJson) as unknown, + }); +} + +const makeRepository = Effect.gen(function* () { + const sql = yield* OrchestrationSql; + + const appendEventRow = SqlSchema.single({ + Request: AppendEventRequestSchema, + Result: EventRowSchema, + execute: (request) => sql` INSERT INTO orchestration_events ( event_id, @@ -76,14 +99,12 @@ const appendEventRow = SqlSchema.single({ command_id AS "commandId", payload_json AS "payloadJson" `, - ), -}); + }); -const readEventRowsFromSequence = SqlSchema.findAll({ - Request: ReadFromSequenceRequestSchema, - Result: EventRowSchema, - execute: (request) => - Effect.flatMap(SqlClient.SqlClient, (sql) => + const readEventRowsFromSequence = SqlSchema.findAll({ + Request: ReadFromSequenceRequestSchema, + Result: EventRowSchema, + execute: (request) => sql` SELECT sequence, @@ -99,92 +120,59 @@ const readEventRowsFromSequence = SqlSchema.findAll({ ORDER BY sequence ASC LIMIT ${request.limit} `, - ), -}); - -function eventRowToOrchestrationEvent(row: EventRow): OrchestrationEvent { - return decodeEvent({ - sequence: row.sequence, - eventId: row.eventId, - type: row.type, - aggregateType: row.aggregateType, - aggregateId: row.aggregateId, - occurredAt: row.occurredAt, - commandId: row.commandId, - payload: JSON.parse(row.payloadJson) as unknown, }); -} - -export function makeSqliteOrchestrationEventRepository( - dbPath: string, -): OrchestrationEventRepositoryShape { - fs.mkdirSync(path.dirname(dbPath), { recursive: true }); - - const runtime: ManagedRuntime.ManagedRuntime = ManagedRuntime.make( - SqliteClient.layer({ - filename: dbPath, - }), - ); - let closed = false; - - const provideSql = ( - effect: Effect.Effect, - ): Effect.Effect => Effect.provide(effect, runtime); - - const initialize = Effect.gen(function* () { - const sql = yield* SqlClient.SqlClient; - yield* sql`PRAGMA journal_mode = WAL;`; - yield* sql`PRAGMA foreign_keys = ON;`; - yield* runMigrations; - }); - - const initialized = runtime.runPromise(initialize).then(() => undefined); - const ensureInitialized = Effect.promise(() => initialized); const append: OrchestrationEventRepositoryShape["append"] = (event) => - Effect.gen(function* () { - yield* ensureInitialized; - const row = yield* provideSql( - appendEventRow({ - eventId: event.eventId, - type: event.type, - aggregateType: event.aggregateType, - aggregateId: event.aggregateId, - occurredAt: event.occurredAt, - commandId: event.commandId, - payloadJson: JSON.stringify(event.payload), - }), - ); - return eventRowToOrchestrationEvent(row); - }).pipe(Effect.orDie); + appendEventRow({ + eventId: event.eventId, + type: event.type, + aggregateType: event.aggregateType, + aggregateId: event.aggregateId, + occurredAt: event.occurredAt, + commandId: event.commandId, + payloadJson: JSON.stringify(event.payload), + }).pipe(Effect.map(eventRowToOrchestrationEvent), Effect.orDie); const readFromSequence: OrchestrationEventRepositoryShape["readFromSequence"] = ( sequenceExclusive, limit = 1_000, ) => - Effect.gen(function* () { - yield* ensureInitialized; - const rows = yield* provideSql( - readEventRowsFromSequence({ - sequenceExclusive, - limit, - }), - ); - return rows.map(eventRowToOrchestrationEvent); - }).pipe(Effect.orDie); - - const close = (): void => { - if (closed) { - return; - } - closed = true; - void runtime.dispose(); - }; + readEventRowsFromSequence({ sequenceExclusive, limit }).pipe( + Effect.map((rows) => rows.map(eventRowToOrchestrationEvent)), + Effect.orDie, + ); return { append, readFromSequence, readAll: () => readFromSequence(0, Number.MAX_SAFE_INTEGER), - close, - }; + } satisfies OrchestrationEventRepositoryShape; +}); + +export const OrchestrationEventRepositoryLive = Layer.effect( + OrchestrationEventRepository, + makeRepository, +); + +export function makeSqliteOrchestrationEventRepositoryLive(dbPath: string) { + const SqliteClientLive = Effect.gen(function* () { + yield* Effect.try({ + try: () => fs.mkdirSync(path.dirname(dbPath), { recursive: true }), + catch: (error) => Effect.die(error), + }) + return SqliteClient.layer({ filename: dbPath }); + }).pipe(Layer.unwrapEffect) + + const OrchestrationSqlLive = Layer.scoped( + OrchestrationSql, + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + yield* sql`PRAGMA journal_mode = WAL;`; + yield* sql`PRAGMA foreign_keys = ON;`; + yield* runMigrations; + return sql; + }), + ).pipe(Layer.provide(SqliteClientLive)); + + return OrchestrationEventRepositoryLive.pipe(Layer.provide(OrchestrationSqlLive)); } diff --git a/apps/server/src/persistence/Services/OrchestrationEvents.ts b/apps/server/src/persistence/Services/OrchestrationEvents.ts index 503df4c18ca4c963f4570c8348cd1b00d3bfffb7..d785733358f4875525c74bbdc9ad894ed80f8080 100644 --- a/apps/server/src/persistence/Services/OrchestrationEvents.ts +++ b/apps/server/src/persistence/Services/OrchestrationEvents.ts @@ -11,7 +11,6 @@ export interface OrchestrationEventRepositoryShape { limit?: number, ) => Effect.Effect; readonly readAll: () => Effect.Effect; - readonly close: () => void; } export class OrchestrationEventRepository extends Context.Tag("orchestration/EventRepository")< diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index cdee56f031e6e9624dfbeae62573461f3aca8911..dd8f3fd6eb1a02e19212168465f01a8f833207d9 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -36,7 +36,7 @@ import { import { TerminalManager } from "./terminalManager"; import { loadResolvedKeybindingsConfig, upsertKeybindingRule } from "./keybindings"; import { searchWorkspaceEntries } from "./workspaceEntries"; -import { createOrchestrationEngine } from "./orchestration/runtime"; +import { createOrchestrationSystem, type OrchestrationSystem } from "./orchestration/runtime"; const MIME_TYPES: Record = { ".html": "text/html; charset=utf-8", @@ -88,6 +88,8 @@ function asNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } +const noop = () => {}; + export function createServer(options: ServerOptions) { const { port, @@ -107,14 +109,15 @@ export function createServer(options: ServerOptions) { const projectRegistry = providedRegistry ?? new ProjectRegistry(path.join(os.homedir(), ".t3", "userdata")); const gitManager = providedGitManager ?? new GitManager(); - const orchestrationEngine = createOrchestrationEngine( - stateDir ?? path.join(os.homedir(), ".t3", "userdata"), - ); + const resolvedStateDir = stateDir ?? path.join(os.homedir(), ".t3", "userdata"); + let orchestrationSystem: OrchestrationSystem | null = null; const clients = new Set(); const logger = createLogger("ws"); const logWebSocketEvents = explicitLogWsEvents ?? parseBooleanEnv(process.env.T3CODE_LOG_WS_EVENTS) ?? Boolean(devUrl); let keybindingsConfig = loadResolvedKeybindingsConfig(logger); + let unsubscribeReadModel = noop; + let unsubscribeDomainEvents = noop; function logOutgoingPush(push: WsPush, recipients: number) { if (!logWebSocketEvents) return; @@ -125,46 +128,62 @@ export function createServer(options: ServerOptions) { }); } - const unsubscribeReadModel = orchestrationEngine.subscribeToReadModel((snapshot) => { - const push: WsPush = { - type: "push", - channel: ORCHESTRATION_WS_CHANNELS.readModel, - data: { - sequence: snapshot.sequence, - snapshot, - } satisfies OrchestrationReadModelPush, - }; - const message = JSON.stringify(push); - let recipients = 0; - for (const client of clients) { - if (client.readyState === client.OPEN) { - client.send(message); - recipients += 1; - } + function requireOrchestrationEngine() { + if (!orchestrationSystem) { + throw new Error("Orchestration engine is not started"); } - logOutgoingPush(push, recipients); - }); + return orchestrationSystem.engine; + } + + function attachOrchestrationSubscriptions() { + const orchestrationEngine = requireOrchestrationEngine(); - const unsubscribeDomainEvents = orchestrationEngine.subscribeToDomainEvents((event) => { - const push: WsPush = { - type: "push", - channel: ORCHESTRATION_WS_CHANNELS.domainEvent, - data: event, - }; - const message = JSON.stringify(push); - let recipients = 0; - for (const client of clients) { - if (client.readyState === client.OPEN) { - client.send(message); - recipients += 1; + unsubscribeReadModel = orchestrationEngine.subscribeToReadModel((snapshot) => { + const push: WsPush = { + type: "push", + channel: ORCHESTRATION_WS_CHANNELS.readModel, + data: { + sequence: snapshot.sequence, + snapshot, + } satisfies OrchestrationReadModelPush, + }; + const message = JSON.stringify(push); + let recipients = 0; + for (const client of clients) { + if (client.readyState === client.OPEN) { + client.send(message); + recipients += 1; + } } - } - logOutgoingPush(push, recipients); - }); + logOutgoingPush(push, recipients); + }); + + unsubscribeDomainEvents = orchestrationEngine.subscribeToDomainEvents((event) => { + const push: WsPush = { + type: "push", + channel: ORCHESTRATION_WS_CHANNELS.domainEvent, + data: event, + }; + const message = JSON.stringify(push); + let recipients = 0; + for (const client of clients) { + if (client.readyState === client.OPEN) { + client.send(message); + recipients += 1; + } + } + logOutgoingPush(push, recipients); + }); + } // Forward provider events to all connected WebSocket clients providerManager.on("event", (event) => { void (async () => { + const liveOrchestrationSystem = orchestrationSystem; + if (!liveOrchestrationSystem) { + return; + } + const orchestrationEngine = liveOrchestrationSystem.engine; const snapshot = orchestrationEngine.getSnapshot(); const thread = snapshot.threads.find((entry) => entry.session?.sessionId === event.sessionId); if (!thread) return; @@ -367,6 +386,7 @@ export function createServer(options: ServerOptions) { wss.on("connection", (ws) => { clients.add(ws); + const orchestrationEngine = requireOrchestrationEngine(); // Send welcome message with project info const segments = cwd.split(/[/\\]/).filter(Boolean); @@ -433,6 +453,8 @@ export function createServer(options: ServerOptions) { } async function routeRequest(request: WsRequest): Promise { + const orchestrationEngine = requireOrchestrationEngine(); + switch (request.method) { case WS_METHODS.providersStartSession: return providerManager.startSession(request.params as never); @@ -676,21 +698,42 @@ export function createServer(options: ServerOptions) { } } - function start() { + async function ensureOrchestrationSystemStarted() { + if (orchestrationSystem) { + return; + } + + orchestrationSystem = await createOrchestrationSystem(resolvedStateDir); + attachOrchestrationSubscriptions(); + } + + async function disposeOrchestrationSystem() { + unsubscribeReadModel(); + unsubscribeDomainEvents(); + unsubscribeReadModel = noop; + unsubscribeDomainEvents = noop; + + if (!orchestrationSystem) { + return; + } + + const system = orchestrationSystem; + orchestrationSystem = null; + await system.dispose(); + } + + async function start() { + await ensureOrchestrationSystemStarted(); + return new Promise((resolve, reject) => { const onError = (error: Error) => { httpServer.off("error", onError); - reject(error); + void disposeOrchestrationSystem().finally(() => reject(error)); }; httpServer.once("error", onError); - const onListening = async () => { + const onListening = () => { httpServer.off("error", onError); - try { - await orchestrationEngine.start(); - resolve(); - } catch (error) { - reject(error instanceof Error ? error : new Error("Failed to start orchestration engine")); - } + resolve(); }; if (host) { httpServer.listen(port, host, onListening); @@ -701,9 +744,7 @@ export function createServer(options: ServerOptions) { } async function stop(): Promise { - unsubscribeReadModel(); - unsubscribeDomainEvents(); - await orchestrationEngine.stop(); + await disposeOrchestrationSystem(); terminalManager.off("event", onTerminalEvent); providerManager.stopAll(); providerManager.dispose(); diff --git a/bun.lock b/bun.lock index be1fceaab7490cbcdb4abe7d68799092e51a3608..2528167f3753aa4332a60612df2b423f7395d3c5 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,8 @@ "t3": "./dist/index.mjs", }, "dependencies": { + "@effect/platform": "^0.94.5", + "@effect/platform-node": "^0.104.1", "@effect/sql": "^0.49.0", "@effect/sql-sqlite-node": "^0.50.1", "effect": "^3.19.19", @@ -148,14 +150,24 @@ "@base-ui/utils": ["@base-ui/utils@0.2.5", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw=="], + "@effect/cluster": ["@effect/cluster@0.56.4", "", { "dependencies": { "kubernetes-types": "^1.30.0" }, "peerDependencies": { "@effect/platform": "^0.94.5", "@effect/rpc": "^0.73.1", "@effect/sql": "^0.49.0", "@effect/workflow": "^0.16.0", "effect": "^3.19.17" } }, "sha512-7Je5/JlbZOlsSxsbKjr97dJed2cNGWsb+TLNgMcr5mRDbcWlFOTUGvsrisEJV6waosYLIg+2omPdvnvRoYKdhA=="], + "@effect/experimental": ["@effect/experimental@0.58.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-IEP9sapjF6rFy5TkoqDPc86st/fnqUfjT7Xa3pWJrFGr1hzaMXHo+mWsYOZS9LAOVKnpHuVziDK97EP5qsCHVA=="], "@effect/platform": ["@effect/platform@0.94.5", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.17" } }, "sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A=="], + "@effect/platform-node": ["@effect/platform-node@0.104.1", "", { "dependencies": { "@effect/platform-node-shared": "^0.57.1", "mime": "^3.0.0", "undici": "^7.10.0", "ws": "^8.18.2" }, "peerDependencies": { "@effect/cluster": "^0.56.1", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "effect": "^3.19.15" } }, "sha512-jT1a/z98niK6fnEU8pWHPPCdJMVDRCIdB65lolcOjse5rsTwVbczMjvKkhVQpF63mNWoOnol7OTRNkw5L54llg=="], + + "@effect/platform-node-shared": ["@effect/platform-node-shared@0.57.1", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "multipasta": "^0.2.7", "ws": "^8.18.2" }, "peerDependencies": { "@effect/cluster": "^0.56.1", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "effect": "^3.19.15" } }, "sha512-oX/bApMdoKsyrDiNdJxo7U9Rz1RXsjRv+ecfAPp1qGlSdGIo32wVRvJ2XCHqYj0sqaYJS0pU0/GCulRfVGuJag=="], + + "@effect/rpc": ["@effect/rpc@0.73.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.94.5", "effect": "^3.19.18" } }, "sha512-td7LHDgBOYKg+VgGWEelD8rSAmvjXz7am17vfxZROX5qIYuvH7drL/z4p5xQFadhHZ7DYdlFpqdO9ggc77OCIw=="], + "@effect/sql": ["@effect/sql@0.49.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.0", "effect": "^3.19.13" } }, "sha512-9UEKR+z+MrI/qMAmSvb/RiD9KlgIazjZUCDSpwNgm0lEK9/Q6ExEyfziiYFVCPiptp52cBw8uBHRic8hHnwqXA=="], "@effect/sql-sqlite-node": ["@effect/sql-sqlite-node@0.50.1", "", { "dependencies": { "better-sqlite3": "^12.6.2" }, "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.4", "@effect/sql": "^0.49.0", "effect": "^3.19.16" } }, "sha512-L7DlHcOVl9/9V5stttFyv9wjvA9PBbaXwsp9pU+KN8KqokWgAVBNBEDf/dVfboUrynbDpDWwMU9SJmRJ1LXASQ=="], + "@effect/workflow": ["@effect/workflow@0.16.0", "", { "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.0", "@effect/rpc": "^0.73.0", "effect": "^3.19.13" } }, "sha512-MiAdlxx3TixkgHdbw+Yf1Z3tHAAE0rOQga12kIydJqj05Fnod+W/I+kQGRMY/XWRg+QUsVxhmh1qTr7Ype6lrw=="], + "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], @@ -336,6 +348,34 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.46.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DJbQsSJUr4KSi9uU0QqOgI7PX2C+fKGZX+YDprt3vM2sC0dWZsgVTLoN2vtkNyEWJSY2mnvRFUshWXT3bmo0Ug=="], + "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], + "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.16", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-McjTuEPuacSIcXdoI2O9W6VSHIOs9ApEHnEUwONKZnKqIo2GGv1vNg9Pr8tgBOL7lgBWNEHX5ROJ5z1X74sENQ=="], "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], @@ -920,6 +960,8 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], @@ -1056,6 +1098,8 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -1342,6 +1386,8 @@ "unconfig-core": ["unconfig-core@7.4.2", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg=="], + "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],