authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-21 20:22:55-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 13:20:36-08:00
logc71e71b5acacb74e201b3f4692ef08ed1dce3a28
tree41607073b3380187d120c668cf8ef977105940cf
parentbbf4e6bd3cf4dc1f73f3a0735092cf2f5e88c347

more effect: scoped layers


11 files changed, 307 insertions(+), 210 deletions(-)

apps/server/package.json+2
......@@ -17,6 +17,8 @@
1717 "test": "vitest run"
1818 },
1919 "dependencies": {
20 "@effect/platform": "^0.94.5",
21 "@effect/platform-node": "^0.104.1",
2022 "@effect/sql": "^0.49.0",
2123 "@effect/sql-sqlite-node": "^0.50.1",
2224 "effect": "^3.19.19",
apps/server/src/orchestration/engine.test.ts+37-27
......@@ -8,6 +8,7 @@ import { afterEach, describe, expect, it } from "vitest";
88
99import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents";
1010import { OrchestrationEngine } from "./engine";
11import { createOrchestrationSystem } from "./runtime";
1112
1213const tempDirs: string[] = [];
1314
......@@ -30,8 +31,8 @@ describe("OrchestrationEngine", () => {
3031 const projectId = "project-1";
3132 const threadId = "thread-1";
3233
33 const engineA = new OrchestrationEngine(stateDir);
34 await engineA.start();
34 const firstSystem = await createOrchestrationSystem(stateDir);
35 const engineA = firstSystem.engine;
3536 await engineA.dispatch({
3637 type: "project.create",
3738 commandId: "cmd-1",
......@@ -63,19 +64,19 @@ describe("OrchestrationEngine", () => {
6364 createdAt,
6465 });
6566 const snapshotA = engineA.getSnapshot();
66 await engineA.stop();
67 await firstSystem.dispose();
6768
68 const engineB = new OrchestrationEngine(stateDir);
69 await engineB.start();
69 const secondSystem = await createOrchestrationSystem(stateDir);
70 const engineB = secondSystem.engine;
7071 const snapshotB = engineB.getSnapshot();
7172 expect(snapshotB).toEqual(snapshotA);
72 await engineB.stop();
73 await secondSystem.dispose();
7374 });
7475
7576 it("fans out read-model updates to subscribers", async () => {
7677 const stateDir = makeTempDir("t3code-orchestration-fanout-");
77 const engine = new OrchestrationEngine(stateDir);
78 await engine.start();
78 const system = await createOrchestrationSystem(stateDir);
79 const engine = system.engine;
7980 const updates: number[] = [];
8081 const unsubscribe = engine.subscribeToReadModel((snapshot) => {
8182 updates.push(snapshot.sequence);
......@@ -91,13 +92,13 @@ describe("OrchestrationEngine", () => {
9192 });
9293 unsubscribe();
9394 expect(updates.length).toBeGreaterThan(0);
94 await engine.stop();
95 await system.dispose();
9596 });
9697
9798 it("replays append-only events from sequence", async () => {
9899 const stateDir = makeTempDir("t3code-orchestration-replay-");
99 const engine = new OrchestrationEngine(stateDir);
100 await engine.start();
100 const system = await createOrchestrationSystem(stateDir);
101 const engine = system.engine;
101102 await engine.dispatch({
102103 type: "project.create",
103104 commandId: "cmd-a",
......@@ -117,14 +118,14 @@ describe("OrchestrationEngine", () => {
117118 expect(events.length).toBe(2);
118119 expect(events[0]?.type).toBe("project.created");
119120 expect(events[1]?.type).toBe("project.deleted");
120 await engine.stop();
121 await system.dispose();
121122 });
122123
123124 it("stores completed turn summaries even when no files changed", async () => {
124125 const stateDir = makeTempDir("t3code-orchestration-turn-diff-");
125 const engine = new OrchestrationEngine(stateDir);
126 const firstSystem = await createOrchestrationSystem(stateDir);
127 const engine = firstSystem.engine;
126128 const completedAt = new Date().toISOString();
127 await engine.start();
128129 await engine.dispatch({
129130 type: "project.create",
130131 commandId: "cmd-project-turn-diff",
......@@ -167,10 +168,10 @@ describe("OrchestrationEngine", () => {
167168 checkpointTurnCount: 1,
168169 },
169170 ]);
170 await engine.stop();
171 await firstSystem.dispose();
171172
172 const restartedEngine = new OrchestrationEngine(stateDir);
173 await restartedEngine.start();
173 const secondSystem = await createOrchestrationSystem(stateDir);
174 const restartedEngine = secondSystem.engine;
174175 const restartedThread = restartedEngine
175176 .getSnapshot()
176177 .threads.find((thread) => thread.id === "thread-turn-diff");
......@@ -183,14 +184,14 @@ describe("OrchestrationEngine", () => {
183184 checkpointTurnCount: 1,
184185 },
185186 ]);
186 await restartedEngine.stop();
187 await secondSystem.dispose();
187188 });
188189
189190 it("reverts thread messages and turn summaries to a checkpoint", async () => {
190191 const stateDir = makeTempDir("t3code-orchestration-revert-");
191 const engine = new OrchestrationEngine(stateDir);
192 const firstSystem = await createOrchestrationSystem(stateDir);
193 const engine = firstSystem.engine;
192194 const createdAt = new Date().toISOString();
193 await engine.start();
194195 await engine.dispatch({
195196 type: "project.create",
196197 commandId: "cmd-project-revert",
......@@ -291,10 +292,10 @@ describe("OrchestrationEngine", () => {
291292 expect(thread?.messages.map((message) => message.id)).toEqual(["user-1", "assistant:turn-1"]);
292293 expect(thread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]);
293294 expect(thread?.latestTurnId).toBe("turn-1");
294 await engine.stop();
295 await firstSystem.dispose();
295296
296 const restarted = new OrchestrationEngine(stateDir);
297 await restarted.start();
297 const secondSystem = await createOrchestrationSystem(stateDir);
298 const restarted = secondSystem.engine;
298299 const restartedThread = restarted
299300 .getSnapshot()
300301 .threads.find((entry) => entry.id === "thread-revert");
......@@ -303,12 +304,22 @@ describe("OrchestrationEngine", () => {
303304 "assistant:turn-1",
304305 ]);
305306 expect(restartedThread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]);
306 await restarted.stop();
307 await secondSystem.dispose();
307308 });
308309
309310 it("allows stop to be called multiple times", async () => {
310 const stateDir = makeTempDir("t3code-orchestration-stop-");
311 const engine = new OrchestrationEngine(stateDir);
311 const inMemoryStore: OrchestrationEventRepositoryShape = {
312 append(event) {
313 return Effect.succeed({ ...event, sequence: 1 });
314 },
315 readFromSequence() {
316 return Effect.succeed([]);
317 },
318 readAll() {
319 return Effect.succeed([]);
320 },
321 };
322 const engine = new OrchestrationEngine(inMemoryStore);
312323 await engine.start();
313324 await engine.stop();
314325 await expect(engine.stop()).resolves.toBeUndefined();
......@@ -341,7 +352,6 @@ describe("OrchestrationEngine", () => {
341352 readAll() {
342353 return Effect.succeed(events);
343354 },
344 close() {},
345355 };
346356
347357 const engine = new OrchestrationEngine(flakyStore);
apps/server/src/orchestration/engine.ts+6-13
......@@ -1,5 +1,3 @@
1import path from "node:path";
2
31import type {
42 OrchestrationCommand,
53 OrchestrationEvent,
......@@ -21,7 +19,6 @@ import {
2119
2220import { createLogger } from "../logger";
2321import type { OrchestrationEventRepositoryShape } from "../persistence/Services/OrchestrationEvents";
24import { makeSqliteOrchestrationEventRepository } from "../persistence/Layers/OrchestrationEvents";
2522import { createEmptyReadModel, reduceEvent } from "./reducer";
2623import { UI_ENTITY_CONTRACTS } from "./uiContractInventory";
2724
......@@ -215,15 +212,8 @@ export class OrchestrationEngine {
215212 private readonly readModelListeners = new Set<(snapshot: OrchestrationReadModel) => void>();
216213 private readonly domainEventListeners = new Set<(event: OrchestrationEvent) => void>();
217214
218 constructor(stateDir: string);
219 constructor(eventStore: OrchestrationEventRepositoryShape);
220 constructor(stateDirOrEventStore: string | OrchestrationEventRepositoryShape) {
221 if (typeof stateDirOrEventStore === "string") {
222 const dbPath = path.join(stateDirOrEventStore, "orchestration.sqlite");
223 this.eventStore = makeSqliteOrchestrationEventRepository(dbPath);
224 } else {
225 this.eventStore = stateDirOrEventStore;
226 }
215 constructor(eventStore: OrchestrationEventRepositoryShape) {
216 this.eventStore = eventStore;
227217 this.readModel = createEmptyReadModel(new Date().toISOString());
228218 this.commandQueue = Runtime.runSync(this.runtime)(Queue.unbounded<CommandEnvelope>());
229219 this.readModelPubSub = Runtime.runSync(this.runtime)(
......@@ -279,6 +269,10 @@ export class OrchestrationEngine {
279269 }
280270
281271 async start(): Promise<void> {
272 if (this.workerFiber) {
273 return;
274 }
275
282276 await Runtime.runPromise(this.runtime)(this.bootstrapReadModel());
283277
284278 const worker = Stream.fromQueue(this.commandQueue).pipe(
......@@ -297,7 +291,6 @@ export class OrchestrationEngine {
297291 await Runtime.runPromise(this.runtime)(Fiber.interrupt(this.workerFiber));
298292 this.workerFiber = null;
299293 }
300 this.eventStore.close();
301294 }
302295
303296 getSnapshot(): OrchestrationReadModel {
apps/server/src/orchestration/layers.test.ts+7-7
......@@ -4,7 +4,7 @@ import path from "node:path";
44
55import { afterEach, describe, expect, it } from "vitest";
66
7import { createOrchestrationEngine } from "./runtime";
7import { createOrchestrationSystem } from "./runtime";
88
99const tempDirs: string[] = [];
1010
......@@ -24,9 +24,9 @@ describe("orchestration layers", () => {
2424 it("creates an engine through Context.Tag wiring", async () => {
2525 const stateDir = makeTempDir("t3code-orchestration-layer-");
2626 const createdAt = new Date().toISOString();
27 const engine = createOrchestrationEngine(stateDir);
27 const firstSystem = await createOrchestrationSystem(stateDir);
28 const { engine } = firstSystem;
2829
29 await engine.start();
3030 await engine.dispatch({
3131 type: "project.create",
3232 commandId: "cmd-layer-1",
......@@ -37,11 +37,11 @@ describe("orchestration layers", () => {
3737 createdAt,
3838 });
3939 const firstSnapshot = engine.getSnapshot();
40 await engine.stop();
40 await firstSystem.dispose();
4141
42 const restarted = createOrchestrationEngine(stateDir);
43 await restarted.start();
42 const secondSystem = await createOrchestrationSystem(stateDir);
43 const restarted = secondSystem.engine;
4444 expect(restarted.getSnapshot()).toEqual(firstSnapshot);
45 await restarted.stop();
45 await secondSystem.dispose();
4646 });
4747});
apps/server/src/orchestration/layers.ts+13-8
......@@ -3,23 +3,28 @@ import path from "node:path";
33import { Effect, Layer } from "effect";
44
55import { OrchestrationEventRepository } from "../persistence/Services/OrchestrationEvents";
6import { makeSqliteOrchestrationEventRepository } from "../persistence/Layers/OrchestrationEvents";
6import { makeSqliteOrchestrationEventRepositoryLive } from "../persistence/Layers/OrchestrationEvents";
77import { OrchestrationEngine } from "./engine";
88import { OrchestrationConfig, OrchestrationEngineService } from "./services";
99
10export const OrchestrationEventRepositoryLive = Layer.effect(
11 OrchestrationEventRepository,
10export const OrchestrationEventRepositoryLive = Layer.unwrapEffect(
1211 Effect.map(OrchestrationConfig, ({ stateDir }) => {
1312 const dbPath = path.join(stateDir, "orchestration.sqlite");
14 return makeSqliteOrchestrationEventRepository(dbPath);
13 return makeSqliteOrchestrationEventRepositoryLive(dbPath);
1514 }),
1615);
1716
18export const OrchestrationEngineLive = Layer.effect(
17export const OrchestrationEngineLive = Layer.scoped(
1918 OrchestrationEngineService,
20 Effect.map(OrchestrationEventRepository, (eventRepository) => {
21 return new OrchestrationEngine(eventRepository);
22 }),
19 Effect.acquireRelease(
20 Effect.gen(function* () {
21 const eventRepository = yield* OrchestrationEventRepository;
22 const engine = new OrchestrationEngine(eventRepository);
23 yield* Effect.promise(() => engine.start());
24 return engine;
25 }),
26 (engine) => Effect.promise(() => engine.stop()),
27 ),
2328);
2429
2530export const OrchestrationLive = OrchestrationEngineLive.pipe(
apps/server/src/orchestration/runtime.ts+13-5
......@@ -1,14 +1,22 @@
1import { Effect, Layer, Runtime } from "effect";
1import { Layer, ManagedRuntime } from "effect";
22
33import type { OrchestrationEngine } from "./engine";
44import { OrchestrationLive } from "./layers";
55import { OrchestrationConfig, OrchestrationEngineService } from "./services";
66
7export function createOrchestrationEngine(stateDir: string): OrchestrationEngine {
7export interface OrchestrationSystem {
8 readonly engine: OrchestrationEngine;
9 readonly dispose: () => Promise<void>;
10}
11
12export async function createOrchestrationSystem(stateDir: string): Promise<OrchestrationSystem> {
813 const orchestrationLayer = OrchestrationLive.pipe(
914 Layer.provide(Layer.succeed(OrchestrationConfig, { stateDir })),
1015 );
11 return Runtime.runSync(Runtime.defaultRuntime)(
12 Effect.provide(OrchestrationEngineService, orchestrationLayer),
13 );
16 const runtime = ManagedRuntime.make(orchestrationLayer);
17 const engine = await runtime.runPromise(OrchestrationEngineService);
18 return {
19 engine,
20 dispose: () => runtime.dispose(),
21 };
1422}
apps/server/src/persistence/Layers/OrchestrationEvents.test.ts+16-11
......@@ -3,10 +3,11 @@ import os from "node:os";
33import path from "node:path";
44import { DatabaseSync } from "node:sqlite";
55
6import { Runtime } from "effect";
6import { Runtime, ManagedRuntime } from "effect";
77import { afterEach, describe, expect, it } from "vitest";
88
9import { makeSqliteOrchestrationEventRepository } from "./OrchestrationEvents";
9import { OrchestrationEventRepository } from "../Services/OrchestrationEvents";
10import { makeSqliteOrchestrationEventRepositoryLive } from "./OrchestrationEvents";
1011
1112const tempDirs: string[] = [];
1213
......@@ -22,14 +23,15 @@ afterEach(() => {
2223 }
2324});
2425
25describe("SqliteEventStore", () => {
26describe("OrchestrationEventRepository", () => {
2627 it("persists and replays events across restarts", async () => {
2728 const stateDir = makeTempDir("t3code-event-store-");
2829 const dbPath = path.join(stateDir, "orchestration.sqlite");
2930 const runtime = Runtime.defaultRuntime;
3031 const createdAt = new Date().toISOString();
3132
32 const first = makeSqliteOrchestrationEventRepository(dbPath);
33 const firstRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath));
34 const first = await firstRuntime.runPromise(OrchestrationEventRepository);
3335 const saved = await Runtime.runPromise(runtime)(
3436 first.append({
3537 eventId: "event-1",
......@@ -42,12 +44,13 @@ describe("SqliteEventStore", () => {
4244 }),
4345 );
4446 expect(saved.sequence).toBe(1);
45 first.close();
47 await firstRuntime.dispose();
4648
47 const second = makeSqliteOrchestrationEventRepository(dbPath);
49 const secondRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath));
50 const second = await secondRuntime.runPromise(OrchestrationEventRepository);
4851 const replayed = await Runtime.runPromise(runtime)(second.readFromSequence(0));
4952 expect(replayed).toEqual([saved]);
50 second.close();
53 await secondRuntime.dispose();
5154 });
5255
5356 it("creates and reuses the migrator tracking table", async () => {
......@@ -55,13 +58,15 @@ describe("SqliteEventStore", () => {
5558 const dbPath = path.join(stateDir, "orchestration.sqlite");
5659 const runtime = Runtime.defaultRuntime;
5760
58 const first = makeSqliteOrchestrationEventRepository(dbPath);
61 const firstRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath));
62 const first = await firstRuntime.runPromise(OrchestrationEventRepository);
5963 await Runtime.runPromise(runtime)(first.readAll());
60 first.close();
64 await firstRuntime.dispose();
6165
62 const second = makeSqliteOrchestrationEventRepository(dbPath);
66 const secondRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath));
67 const second = await secondRuntime.runPromise(OrchestrationEventRepository);
6368 await Runtime.runPromise(runtime)(second.readAll());
64 second.close();
69 await secondRuntime.dispose();
6570
6671 const db = new DatabaseSync(dbPath, { readOnly: true });
6772 const tableRow = db
apps/server/src/persistence/Layers/OrchestrationEvents.ts+77-89
......@@ -7,10 +7,13 @@ import type { SqlClient as SqlClientService } from "@effect/sql/SqlClient";
77import * as SqlClient from "@effect/sql/SqlClient";
88import * as SqlSchema from "@effect/sql/SqlSchema";
99import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient";
10import { Effect, ManagedRuntime, Schema } from "effect";
10import { Context, Effect, Layer, Schema } from "effect";
1111
12import type { OrchestrationEventRepositoryShape } from "../Services/OrchestrationEvents";
1312import { runMigrations } from "../Migrations";
13import {
14 OrchestrationEventRepository,
15 type OrchestrationEventRepositoryShape,
16} from "../Services/OrchestrationEvents";
1417
1518const decodeEvent = Schema.decodeUnknownSync(OrchestrationEventSchema);
1619
......@@ -42,11 +45,31 @@ const ReadFromSequenceRequestSchema = Schema.Struct({
4245 limit: Schema.Number,
4346});
4447
45const appendEventRow = SqlSchema.single({
46 Request: AppendEventRequestSchema,
47 Result: EventRowSchema,
48 execute: (request) =>
49 Effect.flatMap(SqlClient.SqlClient, (sql) =>
48class OrchestrationSql extends Context.Tag("persistence/OrchestrationSql")<
49 OrchestrationSql,
50 SqlClientService
51>() {}
52
53function eventRowToOrchestrationEvent(row: EventRow): OrchestrationEvent {
54 return decodeEvent({
55 sequence: row.sequence,
56 eventId: row.eventId,
57 type: row.type,
58 aggregateType: row.aggregateType,
59 aggregateId: row.aggregateId,
60 occurredAt: row.occurredAt,
61 commandId: row.commandId,
62 payload: JSON.parse(row.payloadJson) as unknown,
63 });
64}
65
66const makeRepository = Effect.gen(function* () {
67 const sql = yield* OrchestrationSql;
68
69 const appendEventRow = SqlSchema.single({
70 Request: AppendEventRequestSchema,
71 Result: EventRowSchema,
72 execute: (request) =>
5073 sql`
5174 INSERT INTO orchestration_events (
5275 event_id,
......@@ -76,14 +99,12 @@ const appendEventRow = SqlSchema.single({
7699 command_id AS "commandId",
77100 payload_json AS "payloadJson"
78101 `,
79 ),
80});
102 });
81103
82const readEventRowsFromSequence = SqlSchema.findAll({
83 Request: ReadFromSequenceRequestSchema,
84 Result: EventRowSchema,
85 execute: (request) =>
86 Effect.flatMap(SqlClient.SqlClient, (sql) =>
104 const readEventRowsFromSequence = SqlSchema.findAll({
105 Request: ReadFromSequenceRequestSchema,
106 Result: EventRowSchema,
107 execute: (request) =>
87108 sql`
88109 SELECT
89110 sequence,
......@@ -99,92 +120,59 @@ const readEventRowsFromSequence = SqlSchema.findAll({
99120 ORDER BY sequence ASC
100121 LIMIT ${request.limit}
101122 `,
102 ),
103});
104
105function eventRowToOrchestrationEvent(row: EventRow): OrchestrationEvent {
106 return decodeEvent({
107 sequence: row.sequence,
108 eventId: row.eventId,
109 type: row.type,
110 aggregateType: row.aggregateType,
111 aggregateId: row.aggregateId,
112 occurredAt: row.occurredAt,
113 commandId: row.commandId,
114 payload: JSON.parse(row.payloadJson) as unknown,
115123 });
116}
117
118export function makeSqliteOrchestrationEventRepository(
119 dbPath: string,
120): OrchestrationEventRepositoryShape {
121 fs.mkdirSync(path.dirname(dbPath), { recursive: true });
122
123 const runtime: ManagedRuntime.ManagedRuntime<SqlClientService, unknown> = ManagedRuntime.make(
124 SqliteClient.layer({
125 filename: dbPath,
126 }),
127 );
128 let closed = false;
129
130 const provideSql = <A, E>(
131 effect: Effect.Effect<A, E, SqlClientService>,
132 ): Effect.Effect<A, E | unknown> => Effect.provide(effect, runtime);
133
134 const initialize = Effect.gen(function* () {
135 const sql = yield* SqlClient.SqlClient;
136 yield* sql`PRAGMA journal_mode = WAL;`;
137 yield* sql`PRAGMA foreign_keys = ON;`;
138 yield* runMigrations;
139 });
140
141 const initialized = runtime.runPromise(initialize).then(() => undefined);
142 const ensureInitialized = Effect.promise(() => initialized);
143124
144125 const append: OrchestrationEventRepositoryShape["append"] = (event) =>
145 Effect.gen(function* () {
146 yield* ensureInitialized;
147 const row = yield* provideSql(
148 appendEventRow({
149 eventId: event.eventId,
150 type: event.type,
151 aggregateType: event.aggregateType,
152 aggregateId: event.aggregateId,
153 occurredAt: event.occurredAt,
154 commandId: event.commandId,
155 payloadJson: JSON.stringify(event.payload),
156 }),
157 );
158 return eventRowToOrchestrationEvent(row);
159 }).pipe(Effect.orDie);
126 appendEventRow({
127 eventId: event.eventId,
128 type: event.type,
129 aggregateType: event.aggregateType,
130 aggregateId: event.aggregateId,
131 occurredAt: event.occurredAt,
132 commandId: event.commandId,
133 payloadJson: JSON.stringify(event.payload),
134 }).pipe(Effect.map(eventRowToOrchestrationEvent), Effect.orDie);
160135
161136 const readFromSequence: OrchestrationEventRepositoryShape["readFromSequence"] = (
162137 sequenceExclusive,
163138 limit = 1_000,
164139 ) =>
165 Effect.gen(function* () {
166 yield* ensureInitialized;
167 const rows = yield* provideSql(
168 readEventRowsFromSequence({
169 sequenceExclusive,
170 limit,
171 }),
172 );
173 return rows.map(eventRowToOrchestrationEvent);
174 }).pipe(Effect.orDie);
175
176 const close = (): void => {
177 if (closed) {
178 return;
179 }
180 closed = true;
181 void runtime.dispose();
182 };
140 readEventRowsFromSequence({ sequenceExclusive, limit }).pipe(
141 Effect.map((rows) => rows.map(eventRowToOrchestrationEvent)),
142 Effect.orDie,
143 );
183144
184145 return {
185146 append,
186147 readFromSequence,
187148 readAll: () => readFromSequence(0, Number.MAX_SAFE_INTEGER),
188 close,
189 };
149 } satisfies OrchestrationEventRepositoryShape;
150});
151
152export const OrchestrationEventRepositoryLive = Layer.effect(
153 OrchestrationEventRepository,
154 makeRepository,
155);
156
157export function makeSqliteOrchestrationEventRepositoryLive(dbPath: string) {
158 const SqliteClientLive = Effect.gen(function* () {
159 yield* Effect.try({
160 try: () => fs.mkdirSync(path.dirname(dbPath), { recursive: true }),
161 catch: (error) => Effect.die(error),
162 })
163 return SqliteClient.layer({ filename: dbPath });
164 }).pipe(Layer.unwrapEffect)
165
166 const OrchestrationSqlLive = Layer.scoped(
167 OrchestrationSql,
168 Effect.gen(function* () {
169 const sql = yield* SqlClient.SqlClient;
170 yield* sql`PRAGMA journal_mode = WAL;`;
171 yield* sql`PRAGMA foreign_keys = ON;`;
172 yield* runMigrations;
173 return sql;
174 }),
175 ).pipe(Layer.provide(SqliteClientLive));
176
177 return OrchestrationEventRepositoryLive.pipe(Layer.provide(OrchestrationSqlLive));
190178}
apps/server/src/persistence/Services/OrchestrationEvents.ts-1
......@@ -11,7 +11,6 @@ export interface OrchestrationEventRepositoryShape {
1111 limit?: number,
1212 ) => Effect.Effect<OrchestrationEvent[]>;
1313 readonly readAll: () => Effect.Effect<OrchestrationEvent[]>;
14 readonly close: () => void;
1514}
1615
1716export class OrchestrationEventRepository extends Context.Tag("orchestration/EventRepository")<
apps/server/src/wsServer.ts+90-49
......@@ -36,7 +36,7 @@ import {
3636import { TerminalManager } from "./terminalManager";
3737import { loadResolvedKeybindingsConfig, upsertKeybindingRule } from "./keybindings";
3838import { searchWorkspaceEntries } from "./workspaceEntries";
39import { createOrchestrationEngine } from "./orchestration/runtime";
39import { createOrchestrationSystem, type OrchestrationSystem } from "./orchestration/runtime";
4040
4141const MIME_TYPES: Record<string, string> = {
4242 ".html": "text/html; charset=utf-8",
......@@ -88,6 +88,8 @@ function asNumber(value: unknown): number | null {
8888 return typeof value === "number" && Number.isFinite(value) ? value : null;
8989}
9090
91const noop = () => {};
92
9193export function createServer(options: ServerOptions) {
9294 const {
9395 port,
......@@ -107,14 +109,15 @@ export function createServer(options: ServerOptions) {
107109 const projectRegistry =
108110 providedRegistry ?? new ProjectRegistry(path.join(os.homedir(), ".t3", "userdata"));
109111 const gitManager = providedGitManager ?? new GitManager();
110 const orchestrationEngine = createOrchestrationEngine(
111 stateDir ?? path.join(os.homedir(), ".t3", "userdata"),
112 );
112 const resolvedStateDir = stateDir ?? path.join(os.homedir(), ".t3", "userdata");
113 let orchestrationSystem: OrchestrationSystem | null = null;
113114 const clients = new Set<WebSocket>();
114115 const logger = createLogger("ws");
115116 const logWebSocketEvents =
116117 explicitLogWsEvents ?? parseBooleanEnv(process.env.T3CODE_LOG_WS_EVENTS) ?? Boolean(devUrl);
117118 let keybindingsConfig = loadResolvedKeybindingsConfig(logger);
119 let unsubscribeReadModel = noop;
120 let unsubscribeDomainEvents = noop;
118121
119122 function logOutgoingPush(push: WsPush, recipients: number) {
120123 if (!logWebSocketEvents) return;
......@@ -125,46 +128,62 @@ export function createServer(options: ServerOptions) {
125128 });
126129 }
127130
128 const unsubscribeReadModel = orchestrationEngine.subscribeToReadModel((snapshot) => {
129 const push: WsPush = {
130 type: "push",
131 channel: ORCHESTRATION_WS_CHANNELS.readModel,
132 data: {
133 sequence: snapshot.sequence,
134 snapshot,
135 } satisfies OrchestrationReadModelPush,
136 };
137 const message = JSON.stringify(push);
138 let recipients = 0;
139 for (const client of clients) {
140 if (client.readyState === client.OPEN) {
141 client.send(message);
142 recipients += 1;
143 }
131 function requireOrchestrationEngine() {
132 if (!orchestrationSystem) {
133 throw new Error("Orchestration engine is not started");
144134 }
145 logOutgoingPush(push, recipients);
146 });
135 return orchestrationSystem.engine;
136 }
147137
148 const unsubscribeDomainEvents = orchestrationEngine.subscribeToDomainEvents((event) => {
149 const push: WsPush = {
150 type: "push",
151 channel: ORCHESTRATION_WS_CHANNELS.domainEvent,
152 data: event,
153 };
154 const message = JSON.stringify(push);
155 let recipients = 0;
156 for (const client of clients) {
157 if (client.readyState === client.OPEN) {
158 client.send(message);
159 recipients += 1;
138 function attachOrchestrationSubscriptions() {
139 const orchestrationEngine = requireOrchestrationEngine();
140
141 unsubscribeReadModel = orchestrationEngine.subscribeToReadModel((snapshot) => {
142 const push: WsPush = {
143 type: "push",
144 channel: ORCHESTRATION_WS_CHANNELS.readModel,
145 data: {
146 sequence: snapshot.sequence,
147 snapshot,
148 } satisfies OrchestrationReadModelPush,
149 };
150 const message = JSON.stringify(push);
151 let recipients = 0;
152 for (const client of clients) {
153 if (client.readyState === client.OPEN) {
154 client.send(message);
155 recipients += 1;
156 }
160157 }
161 }
162 logOutgoingPush(push, recipients);
163 });
158 logOutgoingPush(push, recipients);
159 });
160
161 unsubscribeDomainEvents = orchestrationEngine.subscribeToDomainEvents((event) => {
162 const push: WsPush = {
163 type: "push",
164 channel: ORCHESTRATION_WS_CHANNELS.domainEvent,
165 data: event,
166 };
167 const message = JSON.stringify(push);
168 let recipients = 0;
169 for (const client of clients) {
170 if (client.readyState === client.OPEN) {
171 client.send(message);
172 recipients += 1;
173 }
174 }
175 logOutgoingPush(push, recipients);
176 });
177 }
164178
165179 // Forward provider events to all connected WebSocket clients
166180 providerManager.on("event", (event) => {
167181 void (async () => {
182 const liveOrchestrationSystem = orchestrationSystem;
183 if (!liveOrchestrationSystem) {
184 return;
185 }
186 const orchestrationEngine = liveOrchestrationSystem.engine;
168187 const snapshot = orchestrationEngine.getSnapshot();
169188 const thread = snapshot.threads.find((entry) => entry.session?.sessionId === event.sessionId);
170189 if (!thread) return;
......@@ -367,6 +386,7 @@ export function createServer(options: ServerOptions) {
367386
368387 wss.on("connection", (ws) => {
369388 clients.add(ws);
389 const orchestrationEngine = requireOrchestrationEngine();
370390
371391 // Send welcome message with project info
372392 const segments = cwd.split(/[/\\]/).filter(Boolean);
......@@ -433,6 +453,8 @@ export function createServer(options: ServerOptions) {
433453 }
434454
435455 async function routeRequest(request: WsRequest): Promise<unknown> {
456 const orchestrationEngine = requireOrchestrationEngine();
457
436458 switch (request.method) {
437459 case WS_METHODS.providersStartSession:
438460 return providerManager.startSession(request.params as never);
......@@ -676,21 +698,42 @@ export function createServer(options: ServerOptions) {
676698 }
677699 }
678700
679 function start() {
701 async function ensureOrchestrationSystemStarted() {
702 if (orchestrationSystem) {
703 return;
704 }
705
706 orchestrationSystem = await createOrchestrationSystem(resolvedStateDir);
707 attachOrchestrationSubscriptions();
708 }
709
710 async function disposeOrchestrationSystem() {
711 unsubscribeReadModel();
712 unsubscribeDomainEvents();
713 unsubscribeReadModel = noop;
714 unsubscribeDomainEvents = noop;
715
716 if (!orchestrationSystem) {
717 return;
718 }
719
720 const system = orchestrationSystem;
721 orchestrationSystem = null;
722 await system.dispose();
723 }
724
725 async function start() {
726 await ensureOrchestrationSystemStarted();
727
680728 return new Promise<void>((resolve, reject) => {
681729 const onError = (error: Error) => {
682730 httpServer.off("error", onError);
683 reject(error);
731 void disposeOrchestrationSystem().finally(() => reject(error));
684732 };
685733 httpServer.once("error", onError);
686 const onListening = async () => {
734 const onListening = () => {
687735 httpServer.off("error", onError);
688 try {
689 await orchestrationEngine.start();
690 resolve();
691 } catch (error) {
692 reject(error instanceof Error ? error : new Error("Failed to start orchestration engine"));
693 }
736 resolve();
694737 };
695738 if (host) {
696739 httpServer.listen(port, host, onListening);
......@@ -701,9 +744,7 @@ export function createServer(options: ServerOptions) {
701744 }
702745
703746 async function stop(): Promise<void> {
704 unsubscribeReadModel();
705 unsubscribeDomainEvents();
706 await orchestrationEngine.stop();
747 await disposeOrchestrationSystem();
707748 terminalManager.off("event", onTerminalEvent);
708749 providerManager.stopAll();
709750 providerManager.dispose();
bun.lock+46
......@@ -34,6 +34,8 @@
3434 "t3": "./dist/index.mjs",
3535 },
3636 "dependencies": {
37 "@effect/platform": "^0.94.5",
38 "@effect/platform-node": "^0.104.1",
3739 "@effect/sql": "^0.49.0",
3840 "@effect/sql-sqlite-node": "^0.50.1",
3941 "effect": "^3.19.19",
......@@ -148,14 +150,24 @@
148150
149151 "@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=="],
150152
153 "@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=="],
154
151155 "@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=="],
152156
153157 "@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=="],
154158
159 "@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=="],
160
161 "@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=="],
162
163 "@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=="],
164
155165 "@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=="],
156166
157167 "@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=="],
158168
169 "@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=="],
170
159171 "@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=="],
160172
161173 "@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 @@
336348
337349 "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.46.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DJbQsSJUr4KSi9uU0QqOgI7PX2C+fKGZX+YDprt3vM2sC0dWZsgVTLoN2vtkNyEWJSY2mnvRFUshWXT3bmo0Ug=="],
338350
351 "@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=="],
352
353 "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="],
354
355 "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="],
356
357 "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="],
358
359 "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="],
360
361 "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="],
362
363 "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="],
364
365 "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="],
366
367 "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="],
368
369 "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="],
370
371 "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="],
372
373 "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="],
374
375 "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="],
376
377 "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
378
339379 "@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=="],
340380
341381 "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],
......@@ -920,6 +960,8 @@
920960
921961 "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
922962
963 "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
964
923965 "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=="],
924966
925967 "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
......@@ -1056,6 +1098,8 @@
10561098
10571099 "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
10581100
1101 "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
1102
10591103 "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
10601104
10611105 "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
......@@ -1342,6 +1386,8 @@
13421386
13431387 "unconfig-core": ["unconfig-core@7.4.2", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg=="],
13441388
1389 "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="],
1390
13451391 "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
13461392
13471393 "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=="],