| author | |
| committer | |
| log | fa1663c1314760505d59cb043562345816f5eb1c |
| tree | 606af09d561a3e55f30fc1534ad79e2254a07d80 |
| parent | 9b046feb1edbd95998daaf00dc68ebd229d5bb45 |
8 files changed, 210 insertions(+), 98 deletions(-)
apps/server/src/orchestration/engine.test.ts+2-2| ... | @@ -7,7 +7,7 @@ import { Effect } from "effect"; | ... | @@ -7,7 +7,7 @@ import { Effect } from "effect"; |
| 7 | import { afterEach, describe, expect, it } from "vitest"; | 7 | import { afterEach, describe, expect, it } from "vitest"; |
| 8 | 8 | ||
| 9 | import { OrchestrationEngine } from "./engine"; | 9 | import { OrchestrationEngine } from "./engine"; |
| 10 | import type { OrchestrationEventStore } from "./eventStore"; | 10 | import type { OrchestrationEventRepositoryShape } from "./eventRepository"; |
| 11 | 11 | ||
| 12 | const tempDirs: string[] = []; | 12 | const tempDirs: string[] = []; |
| 13 | 13 | ||
| ... | @@ -319,7 +319,7 @@ describe("OrchestrationEngine", () => { | ... | @@ -319,7 +319,7 @@ describe("OrchestrationEngine", () => { |
| 319 | let nextSequence = 1; | 319 | let nextSequence = 1; |
| 320 | let shouldFailFirstAppend = true; | 320 | let shouldFailFirstAppend = true; |
| 321 | 321 | ||
| 322 | const flakyStore: OrchestrationEventStore = { | 322 | const flakyStore: OrchestrationEventRepositoryShape = { |
| 323 | append(event) { | 323 | append(event) { |
| 324 | if (shouldFailFirstAppend) { | 324 | if (shouldFailFirstAppend) { |
| 325 | shouldFailFirstAppend = false; | 325 | shouldFailFirstAppend = false; |
apps/server/src/orchestration/engine.ts+6-6| ... | @@ -20,8 +20,8 @@ import { | ... | @@ -20,8 +20,8 @@ import { |
| 20 | } from "effect"; | 20 | } from "effect"; |
| 21 | 21 | ||
| 22 | import { createLogger } from "../logger"; | 22 | import { createLogger } from "../logger"; |
| 23 | import type { OrchestrationEventStore } from "./eventStore"; | 23 | import type { OrchestrationEventRepositoryShape } from "./eventRepository"; |
| 24 | import { SqliteEventStore } from "./eventStore"; | 24 | import { makeSqliteOrchestrationEventRepository } from "./eventStore"; |
| 25 | import { createEmptyReadModel, reduceEvent } from "./reducer"; | 25 | import { createEmptyReadModel, reduceEvent } from "./reducer"; |
| 26 | import { UI_ENTITY_CONTRACTS } from "./uiContractInventory"; | 26 | import { UI_ENTITY_CONTRACTS } from "./uiContractInventory"; |
| 27 | 27 | ||
| ... | @@ -205,7 +205,7 @@ function mapCommandToEvent(command: OrchestrationCommand): Omit<OrchestrationEve | ... | @@ -205,7 +205,7 @@ function mapCommandToEvent(command: OrchestrationCommand): Omit<OrchestrationEve |
| 205 | export class OrchestrationEngine { | 205 | export class OrchestrationEngine { |
| 206 | private readonly logger = createLogger("orchestration"); | 206 | private readonly logger = createLogger("orchestration"); |
| 207 | private readonly runtime = Runtime.defaultRuntime; | 207 | private readonly runtime = Runtime.defaultRuntime; |
| 208 | private readonly eventStore: OrchestrationEventStore; | 208 | private readonly eventStore: OrchestrationEventRepositoryShape; |
| 209 | 209 | ||
| 210 | private readModel: OrchestrationReadModel; | 210 | private readModel: OrchestrationReadModel; |
| 211 | private commandQueue: Queue.Queue<CommandEnvelope>; | 211 | private commandQueue: Queue.Queue<CommandEnvelope>; |
| ... | @@ -216,11 +216,11 @@ export class OrchestrationEngine { | ... | @@ -216,11 +216,11 @@ export class OrchestrationEngine { |
| 216 | private readonly domainEventListeners = new Set<(event: OrchestrationEvent) => void>(); | 216 | private readonly domainEventListeners = new Set<(event: OrchestrationEvent) => void>(); |
| 217 | 217 | ||
| 218 | constructor(stateDir: string); | 218 | constructor(stateDir: string); |
| 219 | constructor(eventStore: OrchestrationEventStore); | 219 | constructor(eventStore: OrchestrationEventRepositoryShape); |
| 220 | constructor(stateDirOrEventStore: string | OrchestrationEventStore) { | 220 | constructor(stateDirOrEventStore: string | OrchestrationEventRepositoryShape) { |
| 221 | if (typeof stateDirOrEventStore === "string") { | 221 | if (typeof stateDirOrEventStore === "string") { |
| 222 | const dbPath = path.join(stateDirOrEventStore, "orchestration.sqlite"); | 222 | const dbPath = path.join(stateDirOrEventStore, "orchestration.sqlite"); |
| 223 | this.eventStore = new SqliteEventStore(dbPath); | 223 | this.eventStore = makeSqliteOrchestrationEventRepository(dbPath); |
| 224 | } else { | 224 | } else { |
| 225 | this.eventStore = stateDirOrEventStore; | 225 | this.eventStore = stateDirOrEventStore; |
| 226 | } | 226 | } |
apps/server/src/orchestration/eventRepository.ts created+20| ... | @@ -0,0 +1,20 @@ | ||
| 1 | import type { OrchestrationEvent } from "@t3tools/contracts"; | ||
| 2 | import { Context } from "effect"; | ||
| 3 | import type { Effect } from "effect"; | ||
| 4 | |||
| 5 | export interface OrchestrationEventRepositoryShape { | ||
| 6 | readonly append: ( | ||
| 7 | event: Omit<OrchestrationEvent, "sequence">, | ||
| 8 | ) => Effect.Effect<OrchestrationEvent>; | ||
| 9 | readonly readFromSequence: ( | ||
| 10 | sequenceExclusive: number, | ||
| 11 | limit?: number, | ||
| 12 | ) => Effect.Effect<OrchestrationEvent[]>; | ||
| 13 | readonly readAll: () => Effect.Effect<OrchestrationEvent[]>; | ||
| 14 | readonly close: () => void; | ||
| 15 | } | ||
| 16 | |||
| 17 | export class OrchestrationEventRepository extends Context.Tag("orchestration/EventRepository")< | ||
| 18 | OrchestrationEventRepository, | ||
| 19 | OrchestrationEventRepositoryShape | ||
| 20 | >() {} | ||
apps/server/src/orchestration/eventStore.test.ts created+90| ... | @@ -0,0 +1,90 @@ | ||
| 1 | import fs from "node:fs"; | ||
| 2 | import os from "node:os"; | ||
| 3 | import path from "node:path"; | ||
| 4 | import { DatabaseSync } from "node:sqlite"; | ||
| 5 | |||
| 6 | import { Runtime } from "effect"; | ||
| 7 | import { afterEach, describe, expect, it } from "vitest"; | ||
| 8 | |||
| 9 | import { makeSqliteOrchestrationEventRepository } from "./eventStore"; | ||
| 10 | |||
| 11 | const tempDirs: string[] = []; | ||
| 12 | |||
| 13 | function makeTempDir(prefix: string): string { | ||
| 14 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); | ||
| 15 | tempDirs.push(dir); | ||
| 16 | return dir; | ||
| 17 | } | ||
| 18 | |||
| 19 | afterEach(() => { | ||
| 20 | for (const dir of tempDirs.splice(0, tempDirs.length)) { | ||
| 21 | fs.rmSync(dir, { recursive: true, force: true }); | ||
| 22 | } | ||
| 23 | }); | ||
| 24 | |||
| 25 | describe("SqliteEventStore", () => { | ||
| 26 | it("persists and replays events across restarts", async () => { | ||
| 27 | const stateDir = makeTempDir("t3code-event-store-"); | ||
| 28 | const dbPath = path.join(stateDir, "orchestration.sqlite"); | ||
| 29 | const runtime = Runtime.defaultRuntime; | ||
| 30 | const createdAt = new Date().toISOString(); | ||
| 31 | |||
| 32 | const first = makeSqliteOrchestrationEventRepository(dbPath); | ||
| 33 | const saved = await Runtime.runPromise(runtime)( | ||
| 34 | first.append({ | ||
| 35 | eventId: "event-1", | ||
| 36 | type: "project.created", | ||
| 37 | aggregateType: "project", | ||
| 38 | aggregateId: "project-1", | ||
| 39 | occurredAt: createdAt, | ||
| 40 | commandId: "cmd-1", | ||
| 41 | payload: { id: "project-1", name: "demo" }, | ||
| 42 | }), | ||
| 43 | ); | ||
| 44 | expect(saved.sequence).toBe(1); | ||
| 45 | first.close(); | ||
| 46 | |||
| 47 | const second = makeSqliteOrchestrationEventRepository(dbPath); | ||
| 48 | const replayed = await Runtime.runPromise(runtime)(second.readFromSequence(0)); | ||
| 49 | expect(replayed).toEqual([saved]); | ||
| 50 | second.close(); | ||
| 51 | }); | ||
| 52 | |||
| 53 | it("creates and reuses the migrator tracking table", async () => { | ||
| 54 | const stateDir = makeTempDir("t3code-event-store-migrations-"); | ||
| 55 | const dbPath = path.join(stateDir, "orchestration.sqlite"); | ||
| 56 | const runtime = Runtime.defaultRuntime; | ||
| 57 | |||
| 58 | const first = makeSqliteOrchestrationEventRepository(dbPath); | ||
| 59 | await Runtime.runPromise(runtime)(first.readAll()); | ||
| 60 | first.close(); | ||
| 61 | |||
| 62 | const second = makeSqliteOrchestrationEventRepository(dbPath); | ||
| 63 | await Runtime.runPromise(runtime)(second.readAll()); | ||
| 64 | second.close(); | ||
| 65 | |||
| 66 | const db = new DatabaseSync(dbPath, { readOnly: true }); | ||
| 67 | const tableRow = db | ||
| 68 | .prepare( | ||
| 69 | ` | ||
| 70 | SELECT name | ||
| 71 | FROM sqlite_master | ||
| 72 | WHERE type = 'table' AND name = 'orchestration_sql_migrations' | ||
| 73 | `, | ||
| 74 | ) | ||
| 75 | .get() as { name: string } | undefined; | ||
| 76 | expect(tableRow?.name).toBe("orchestration_sql_migrations"); | ||
| 77 | |||
| 78 | const migrationRows = db | ||
| 79 | .prepare( | ||
| 80 | ` | ||
| 81 | SELECT * | ||
| 82 | FROM orchestration_sql_migrations | ||
| 83 | `, | ||
| 84 | ) | ||
| 85 | .all() as Array<Record<string, unknown>>; | ||
| 86 | expect(migrationRows).toHaveLength(1); | ||
| 87 | expect(Object.values(migrationRows[0] ?? {}).length).toBeGreaterThan(0); | ||
| 88 | db.close(); | ||
| 89 | }); | ||
| 90 | }); | ||
apps/server/src/orchestration/eventStore.ts+51-74| ... | @@ -4,17 +4,13 @@ import path from "node:path"; | ... | @@ -4,17 +4,13 @@ import path from "node:path"; |
| 4 | import type { OrchestrationEvent } from "@t3tools/contracts"; | 4 | import type { OrchestrationEvent } from "@t3tools/contracts"; |
| 5 | import { OrchestrationEventSchema } from "@t3tools/contracts"; | 5 | import { OrchestrationEventSchema } from "@t3tools/contracts"; |
| 6 | import type { SqlClient as SqlClientService } from "@effect/sql/SqlClient"; | 6 | import type { SqlClient as SqlClientService } from "@effect/sql/SqlClient"; |
| 7 | import * as SqlClient from "@effect/sql/SqlClient"; | 7 | import * as SqlClient from "@effect/sql/SqlClient"; |
| 8 | import * as SqlSchema from "@effect/sql/SqlSchema"; | 8 | import * as SqlSchema from "@effect/sql/SqlSchema"; |
| 9 | import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient"; | 9 | import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient"; |
| 10 | import { Effect, ManagedRuntime, Schema } from "effect"; | 10 | import { Effect, ManagedRuntime, Schema } from "effect"; |
| 11 | 11 | ||
| 12 | export interface OrchestrationEventStore { | 12 | import type { OrchestrationEventRepositoryShape } from "./eventRepository"; |
| 13 | append(event: Omit<OrchestrationEvent, "sequence">): Effect.Effect<OrchestrationEvent>; | 13 | import { runOrchestrationMigrations } from "./migrations"; |
| 14 | readFromSequence(sequenceExclusive: number, limit?: number): Effect.Effect<OrchestrationEvent[]>; | ||
| 15 | readAll(): Effect.Effect<OrchestrationEvent[]>; | ||
| 16 | close(): void; | ||
| 17 | } | ||
| 18 | 14 | ||
| 19 | const decodeEvent = Schema.decodeUnknownSync(OrchestrationEventSchema); | 15 | const decodeEvent = Schema.decodeUnknownSync(OrchestrationEventSchema); |
| 20 | 16 | ||
| ... | @@ -119,59 +115,36 @@ function eventRowToOrchestrationEvent(row: EventRow): OrchestrationEvent { | ... | @@ -119,59 +115,36 @@ function eventRowToOrchestrationEvent(row: EventRow): OrchestrationEvent { |
| 119 | }); | 115 | }); |
| 120 | } | 116 | } |
| 121 | 117 | ||
| 122 | export class SqliteEventStore implements OrchestrationEventStore { | 118 | export function makeSqliteOrchestrationEventRepository( |
| 123 | private readonly runtime: ManagedRuntime.ManagedRuntime<SqlClientService, unknown>; | 119 | dbPath: string, |
| 124 | private readonly migrated: Promise<void>; | 120 | ): OrchestrationEventRepositoryShape { |
| 125 | private closed = false; | 121 | fs.mkdirSync(path.dirname(dbPath), { recursive: true }); |
| 126 | 122 | ||
| 127 | constructor(dbPath: string) { | 123 | const runtime: ManagedRuntime.ManagedRuntime<SqlClientService, unknown> = ManagedRuntime.make( |
| 128 | fs.mkdirSync(path.dirname(dbPath), { recursive: true }); | 124 | SqliteClient.layer({ |
| 129 | this.runtime = ManagedRuntime.make( | 125 | filename: dbPath, |
| 130 | SqliteClient.layer({ | 126 | }), |
| 131 | filename: dbPath, | 127 | ); |
| 132 | }), | 128 | let closed = false; |
| 133 | ); | 129 | |
| 134 | this.migrated = this.runtime.runPromise(this.migrate()).then(() => undefined); | 130 | const provideSql = <A, E>( |
| 135 | } | ||
| 136 | |||
| 137 | private provideSql<A, E>( | ||
| 138 | effect: Effect.Effect<A, E, SqlClientService>, | 131 | effect: Effect.Effect<A, E, SqlClientService>, |
| 139 | ): Effect.Effect<A, E | unknown> { | 132 | ): Effect.Effect<A, E | unknown> => Effect.provide(effect, runtime); |
| 140 | return Effect.provide(effect, this.runtime); | 133 | |
| 141 | } | 134 | const initialize = Effect.gen(function* () { |
| 142 | 135 | const sql = yield* SqlClient.SqlClient; | |
| 143 | private ensureMigrated(): Effect.Effect<void> { | 136 | yield* sql`PRAGMA journal_mode = WAL;`; |
| 144 | return Effect.promise(() => this.migrated); | 137 | yield* sql`PRAGMA foreign_keys = ON;`; |
| 145 | } | 138 | yield* runOrchestrationMigrations; |
| 146 | 139 | }); | |
| 147 | private migrate(): Effect.Effect<void, unknown, SqlClientService> { | 140 | |
| 148 | return Effect.gen(function* () { | 141 | const initialized = runtime.runPromise(initialize).then(() => undefined); |
| 149 | const sql = yield* SqlClient.SqlClient; | 142 | const ensureInitialized = Effect.promise(() => initialized); |
| 150 | yield* sql`PRAGMA journal_mode = WAL;`; | 143 | |
| 151 | yield* sql`PRAGMA foreign_keys = ON;`; | 144 | const append: OrchestrationEventRepositoryShape["append"] = (event) => |
| 152 | yield* sql` | 145 | Effect.gen(function* () { |
| 153 | CREATE TABLE IF NOT EXISTS orchestration_events ( | 146 | yield* ensureInitialized; |
| 154 | sequence INTEGER PRIMARY KEY AUTOINCREMENT, | 147 | const row = yield* provideSql( |
| 155 | event_id TEXT NOT NULL UNIQUE, | ||
| 156 | event_type TEXT NOT NULL, | ||
| 157 | aggregate_type TEXT NOT NULL, | ||
| 158 | aggregate_id TEXT NOT NULL, | ||
| 159 | occurred_at TEXT NOT NULL, | ||
| 160 | command_id TEXT, | ||
| 161 | payload_json TEXT NOT NULL | ||
| 162 | ) | ||
| 163 | `; | ||
| 164 | yield* sql` | ||
| 165 | CREATE INDEX IF NOT EXISTS idx_orch_events_aggregate | ||
| 166 | ON orchestration_events(aggregate_type, aggregate_id, sequence) | ||
| 167 | `; | ||
| 168 | }); | ||
| 169 | } | ||
| 170 | |||
| 171 | append(event: Omit<OrchestrationEvent, "sequence">): Effect.Effect<OrchestrationEvent> { | ||
| 172 | return Effect.gen(this, function* () { | ||
| 173 | yield* this.ensureMigrated(); | ||
| 174 | const row = yield* this.provideSql( | ||
| 175 | appendEventRow({ | 148 | appendEventRow({ |
| 176 | eventId: event.eventId, | 149 | eventId: event.eventId, |
| 177 | type: event.type, | 150 | type: event.type, |
| ... | @@ -184,12 +157,14 @@ export class SqliteEventStore implements OrchestrationEventStore { | ... | @@ -184,12 +157,14 @@ export class SqliteEventStore implements OrchestrationEventStore { |
| 184 | ); | 157 | ); |
| 185 | return eventRowToOrchestrationEvent(row); | 158 | return eventRowToOrchestrationEvent(row); |
| 186 | }).pipe(Effect.orDie); | 159 | }).pipe(Effect.orDie); |
| 187 | } | ||
| 188 | 160 | ||
| 189 | readFromSequence(sequenceExclusive: number, limit = 1_000): Effect.Effect<OrchestrationEvent[]> { | 161 | const readFromSequence: OrchestrationEventRepositoryShape["readFromSequence"] = ( |
| 190 | return Effect.gen(this, function* () { | 162 | sequenceExclusive, |
| 191 | yield* this.ensureMigrated(); | 163 | limit = 1_000, |
| 192 | const rows = yield* this.provideSql( | 164 | ) => |
| 165 | Effect.gen(function* () { | ||
| 166 | yield* ensureInitialized; | ||
| 167 | const rows = yield* provideSql( | ||
| 193 | readEventRowsFromSequence({ | 168 | readEventRowsFromSequence({ |
| 194 | sequenceExclusive, | 169 | sequenceExclusive, |
| 195 | limit, | 170 | limit, |
| ... | @@ -197,17 +172,19 @@ export class SqliteEventStore implements OrchestrationEventStore { | ... | @@ -197,17 +172,19 @@ export class SqliteEventStore implements OrchestrationEventStore { |
| 197 | ); | 172 | ); |
| 198 | return rows.map(eventRowToOrchestrationEvent); | 173 | return rows.map(eventRowToOrchestrationEvent); |
| 199 | }).pipe(Effect.orDie); | 174 | }).pipe(Effect.orDie); |
| 200 | } | ||
| 201 | |||
| 202 | readAll(): Effect.Effect<OrchestrationEvent[]> { | ||
| 203 | return this.readFromSequence(0, Number.MAX_SAFE_INTEGER); | ||
| 204 | } | ||
| 205 | 175 | ||
| 206 | close(): void { | 176 | const close = (): void => { |
| 207 | if (this.closed) { | 177 | if (closed) { |
| 208 | return; | 178 | return; |
| 209 | } | 179 | } |
| 210 | this.closed = true; | 180 | closed = true; |
| 211 | void this.runtime.dispose(); | 181 | void runtime.dispose(); |
| 212 | } | 182 | }; |
| 183 | |||
| 184 | return { | ||
| 185 | append, | ||
| 186 | readFromSequence, | ||
| 187 | readAll: () => readFromSequence(0, Number.MAX_SAFE_INTEGER), | ||
| 188 | close, | ||
| 189 | }; | ||
| 213 | } | 190 | } |
apps/server/src/orchestration/layers.ts+10-11| ... | @@ -2,27 +2,26 @@ import path from "node:path"; | ... | @@ -2,27 +2,26 @@ import path from "node:path"; |
| 2 | 2 | ||
| 3 | import { Effect, Layer } from "effect"; | 3 | import { Effect, Layer } from "effect"; |
| 4 | 4 | ||
| 5 | import { OrchestrationEventRepository } from "./eventRepository"; | ||
| 5 | import { OrchestrationEngine } from "./engine"; | 6 | import { OrchestrationEngine } from "./engine"; |
| 6 | import { SqliteEventStore } from "./eventStore"; | 7 | import { makeSqliteOrchestrationEventRepository } from "./eventStore"; |
| 7 | import { | 8 | import { OrchestrationConfig, OrchestrationEngineService } from "./services"; |
| 8 | OrchestrationConfig, | ||
| 9 | OrchestrationEngineService, | ||
| 10 | OrchestrationEventStoreService, | ||
| 11 | } from "./services"; | ||
| 12 | 9 | ||
| 13 | export const OrchestrationEventStoreLive = Layer.effect( | 10 | export const OrchestrationEventRepositoryLive = Layer.effect( |
| 14 | OrchestrationEventStoreService, | 11 | OrchestrationEventRepository, |
| 15 | Effect.map(OrchestrationConfig, ({ stateDir }) => { | 12 | Effect.map(OrchestrationConfig, ({ stateDir }) => { |
| 16 | const dbPath = path.join(stateDir, "orchestration.sqlite"); | 13 | const dbPath = path.join(stateDir, "orchestration.sqlite"); |
| 17 | return new SqliteEventStore(dbPath); | 14 | return makeSqliteOrchestrationEventRepository(dbPath); |
| 18 | }), | 15 | }), |
| 19 | ); | 16 | ); |
| 20 | 17 | ||
| 21 | export const OrchestrationEngineLive = Layer.effect( | 18 | export const OrchestrationEngineLive = Layer.effect( |
| 22 | OrchestrationEngineService, | 19 | OrchestrationEngineService, |
| 23 | Effect.map(OrchestrationEventStoreService, (eventStore) => new OrchestrationEngine(eventStore)), | 20 | Effect.map(OrchestrationEventRepository, (eventRepository) => { |
| 21 | return new OrchestrationEngine(eventRepository); | ||
| 22 | }), | ||
| 24 | ); | 23 | ); |
| 25 | 24 | ||
| 26 | export const OrchestrationLive = OrchestrationEngineLive.pipe( | 25 | export const OrchestrationLive = OrchestrationEngineLive.pipe( |
| 27 | Layer.provide(OrchestrationEventStoreLive), | 26 | Layer.provide(OrchestrationEventRepositoryLive), |
| 28 | ); | 27 | ); |
apps/server/src/orchestration/migrations.ts created+31| ... | @@ -0,0 +1,31 @@ | ||
| 1 | import * as Migrator from "@effect/sql/Migrator"; | ||
| 2 | import * as SqlClient from "@effect/sql/SqlClient"; | ||
| 3 | import { Effect } from "effect"; | ||
| 4 | |||
| 5 | const ORCHESTRATION_MIGRATIONS = Migrator.fromRecord({ | ||
| 6 | "0001_create_orchestration_events": Effect.flatMap(SqlClient.SqlClient, (sql) => | ||
| 7 | Effect.gen(function* () { | ||
| 8 | yield* sql` | ||
| 9 | CREATE TABLE IF NOT EXISTS orchestration_events ( | ||
| 10 | sequence INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 11 | event_id TEXT NOT NULL UNIQUE, | ||
| 12 | event_type TEXT NOT NULL, | ||
| 13 | aggregate_type TEXT NOT NULL, | ||
| 14 | aggregate_id TEXT NOT NULL, | ||
| 15 | occurred_at TEXT NOT NULL, | ||
| 16 | command_id TEXT, | ||
| 17 | payload_json TEXT NOT NULL | ||
| 18 | ) | ||
| 19 | `; | ||
| 20 | yield* sql` | ||
| 21 | CREATE INDEX IF NOT EXISTS idx_orch_events_aggregate | ||
| 22 | ON orchestration_events(aggregate_type, aggregate_id, sequence) | ||
| 23 | `; | ||
| 24 | }), | ||
| 25 | ), | ||
| 26 | }); | ||
| 27 | |||
| 28 | export const runOrchestrationMigrations = Migrator.make({})({ | ||
| 29 | loader: ORCHESTRATION_MIGRATIONS, | ||
| 30 | table: "orchestration_sql_migrations", | ||
| 31 | }); | ||
apps/server/src/orchestration/services.ts-5| ... | @@ -1,7 +1,6 @@ | ... | @@ -1,7 +1,6 @@ |
| 1 | import { Context } from "effect"; | 1 | import { Context } from "effect"; |
| 2 | 2 | ||
| 3 | import { OrchestrationEngine } from "./engine"; | 3 | import { OrchestrationEngine } from "./engine"; |
| 4 | import type { OrchestrationEventStore } from "./eventStore"; | ||
| 5 | 4 | ||
| 6 | export interface OrchestrationConfigShape { | 5 | export interface OrchestrationConfigShape { |
| 7 | readonly stateDir: string; | 6 | readonly stateDir: string; |
| ... | @@ -12,10 +11,6 @@ export class OrchestrationConfig extends Context.Tag("orchestration/Config")< | ... | @@ -12,10 +11,6 @@ export class OrchestrationConfig extends Context.Tag("orchestration/Config")< |
| 12 | OrchestrationConfigShape | 11 | OrchestrationConfigShape |
| 13 | >() {} | 12 | >() {} |
| 14 | 13 | ||
| 15 | export class OrchestrationEventStoreService extends Context.Tag( | ||
| 16 | "orchestration/EventStore", | ||
| 17 | )<OrchestrationEventStoreService, OrchestrationEventStore>() {} | ||
| 18 | |||
| 19 | export class OrchestrationEngineService extends Context.Tag("orchestration/Engine")< | 14 | export class OrchestrationEngineService extends Context.Tag("orchestration/Engine")< |
| 20 | OrchestrationEngineService, | 15 | OrchestrationEngineService, |
| 21 | OrchestrationEngine | 16 | OrchestrationEngine |