Skip to content

Commit f4c0bd5

Browse files
committed
some work
1 parent a77cc29 commit f4c0bd5

File tree

6 files changed

+498
-157
lines changed

6 files changed

+498
-157
lines changed

src/lib/data/private/migration.remote.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
startDeleteAll,
66
startImageCleanup,
77
} from "$lib/server/services/migration/index.server";
8-
import { getMigrationState, resetMigration } from "$lib/server/services/migration/state.server";
8+
import { migrationActor } from "$lib/server/services/migration/state.server";
99
import type { MigrationState } from "$lib/shared/types/migration";
1010

1111
export const start = command(async (): Promise<{ started: boolean; message: string }> => {
@@ -16,12 +16,12 @@ export const start = command(async (): Promise<{ started: boolean; message: stri
1616
// Use command instead of query to bypass caching for real-time polling
1717
export const getStatus = command(async (): Promise<MigrationState> => {
1818
await requireUtCodeMember();
19-
return getMigrationState();
19+
return migrationActor.getState();
2020
});
2121

2222
export const reset = command(async (): Promise<void> => {
2323
await requireUtCodeMember();
24-
resetMigration();
24+
migrationActor.reset();
2525
});
2626

2727
export const cleanup = command(async (): Promise<{ started: boolean; message: string }> => {
Lines changed: 85 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
/**
22
* Migration orchestrator
33
*
4-
* Coordinates the migration process: cloning repo, running workers,
5-
* and managing state. Uses workers from migration-workers.server.ts.
4+
* Sends events to the migration actor and executes effects.
5+
* The actor handles all state transitions synchronously,
6+
* making race conditions impossible.
67
*/
78

89
import { spawn } from "node:child_process";
910
import { rm } from "node:fs/promises";
1011
import { tmpdir } from "node:os";
1112
import { join } from "node:path";
12-
import { completeMigration, failMigration, isRunning, log, startMigration } from "./state.server";
13+
import type { MigrationOperation } from "$lib/shared/types/migration";
14+
import { type MigrationEffect, migrationActor } from "./state.server";
1315
import {
1416
cleanupInvalidImageUrls,
1517
deleteAllMigratedData,
@@ -21,7 +23,7 @@ import {
2123

2224
const REPO_URL = "https://github.com/ut-code/utcode.net.git";
2325

24-
async function runCommand(cmd: string, args: string[], cwd?: string): Promise<void> {
26+
function runCommand(cmd: string, args: string[], cwd?: string): Promise<void> {
2527
return new Promise((resolve, reject) => {
2628
const child = spawn(cmd, args, { cwd, stdio: "pipe" });
2729
let stderr = "";
@@ -38,112 +40,124 @@ async function runCommand(cmd: string, args: string[], cwd?: string): Promise<vo
3840

3941
async function cloneRepo(): Promise<string> {
4042
const tempDir = join(tmpdir(), `utcode-migration-${Date.now()}`);
41-
log(`Cloning ${REPO_URL}...`);
43+
migrationActor.log(`Cloning ${REPO_URL}...`);
4244
await runCommand("git", ["clone", "--depth", "1", REPO_URL, tempDir]);
43-
log("Repository cloned successfully");
45+
migrationActor.log("Repository cloned successfully");
4446
return tempDir;
4547
}
4648

47-
export function startDataMigration(): { started: boolean; message: string } {
48-
if (isRunning()) {
49-
return { started: false, message: "Migration already in progress" };
50-
}
49+
// ============================================================================
50+
// Effect executors - async work triggered by actor
51+
// ============================================================================
5152

52-
startMigration();
53-
log("=== Data Migration Started ===");
53+
async function executeMigration(): Promise<void> {
54+
let repoPath: string | null = null;
5455

55-
// Run migration in background (fire and forget with proper error handling)
56-
runMigrationAsync().catch(console.error);
56+
try {
57+
repoPath = await cloneRepo();
5758

58-
return { started: true, message: "Migration started" };
59-
}
59+
const log = (msg: string) => migrationActor.log(msg);
60+
const members = await migrateMembers(repoPath, log);
61+
const articles = await migrateArticles(repoPath, log);
62+
const projects = await migrateProjects(repoPath, log);
63+
const images = await migrateImages(repoPath, log);
6064

61-
export function startImageCleanup(): { started: boolean; message: string } {
62-
if (isRunning()) {
63-
return { started: false, message: "Migration already in progress" };
65+
migrationActor.complete({ members, articles, projects, images });
66+
} catch (e) {
67+
const errorMessage = e instanceof Error ? e.message : String(e);
68+
migrationActor.fail(errorMessage);
69+
} finally {
70+
if (repoPath) {
71+
migrationActor.log("Cleaning up temporary files...");
72+
await rm(repoPath, { recursive: true, force: true }).catch(() => {});
73+
}
6474
}
65-
66-
startMigration();
67-
log("=== Image URL Cleanup Started ===");
68-
69-
runCleanupAsync().catch(console.error);
70-
71-
return { started: true, message: "Cleanup started" };
7275
}
7376

74-
async function runCleanupAsync(): Promise<void> {
77+
async function executeCleanup(): Promise<void> {
7578
try {
79+
const log = (msg: string) => migrationActor.log(msg);
7680
const result = await cleanupInvalidImageUrls(log);
7781

78-
log("=== Cleanup Complete ===");
79-
completeMigration({
82+
migrationActor.complete({
8083
members: { created: result.members.cleaned, skipped: result.members.skipped, errors: 0 },
8184
articles: { created: result.articles.cleaned, skipped: result.articles.skipped, errors: 0 },
8285
projects: { created: result.projects.cleaned, skipped: result.projects.skipped, errors: 0 },
8386
images: { created: 0, skipped: 0, errors: 0 },
8487
});
8588
} catch (e) {
8689
const errorMessage = e instanceof Error ? e.message : String(e);
87-
log(`=== Cleanup Failed: ${errorMessage} ===`);
88-
failMigration(errorMessage);
90+
migrationActor.fail(errorMessage);
8991
}
9092
}
9193

92-
export function startDeleteAll(): { started: boolean; message: string } {
93-
if (isRunning()) {
94-
return { started: false, message: "Migration already in progress" };
95-
}
96-
97-
startMigration();
98-
log("=== Delete All Data Started ===");
99-
100-
runDeleteAllAsync().catch(console.error);
101-
102-
return { started: true, message: "Delete started" };
103-
}
104-
105-
async function runDeleteAllAsync(): Promise<void> {
94+
async function executeDelete(): Promise<void> {
10695
try {
96+
const log = (msg: string) => migrationActor.log(msg);
10797
const result = await deleteAllMigratedData(log);
10898

109-
log("=== Delete Complete ===");
110-
completeMigration({
99+
migrationActor.complete({
111100
members: { created: result.members.deleted, skipped: 0, errors: 0 },
112101
articles: { created: result.articles.deleted, skipped: 0, errors: 0 },
113102
projects: { created: result.projects.deleted, skipped: 0, errors: 0 },
114103
images: { created: 0, skipped: 0, errors: 0 },
115104
});
116105
} catch (e) {
117106
const errorMessage = e instanceof Error ? e.message : String(e);
118-
log(`=== Delete Failed: ${errorMessage} ===`);
119-
failMigration(errorMessage);
107+
migrationActor.fail(errorMessage);
120108
}
121109
}
122110

123-
async function runMigrationAsync(): Promise<void> {
124-
let repoPath: string | null = null;
111+
/**
112+
* Execute an effect returned by the actor
113+
*/
114+
function executeEffect(effect: MigrationEffect): void {
115+
if (!effect) return;
116+
117+
switch (effect.type) {
118+
case "RUN_MIGRATION":
119+
executeMigration().catch(console.error);
120+
break;
121+
case "RUN_CLEANUP":
122+
executeCleanup().catch(console.error);
123+
break;
124+
case "RUN_DELETE":
125+
executeDelete().catch(console.error);
126+
break;
127+
}
128+
}
125129

126-
try {
127-
// Clone repo
128-
repoPath = await cloneRepo();
130+
// ============================================================================
131+
// Public API - sends events to actor and executes effects
132+
// ============================================================================
129133

130-
// Run migrations in order (members first, then articles/projects, then images)
131-
const members = await migrateMembers(repoPath, log);
132-
const articles = await migrateArticles(repoPath, log);
133-
const projects = await migrateProjects(repoPath, log);
134-
const images = await migrateImages(repoPath, log);
134+
/**
135+
* Start an operation by sending START event to actor
136+
* Returns immediately - async work runs in background
137+
*/
138+
export function startOperation(operation: MigrationOperation): {
139+
started: boolean;
140+
message: string;
141+
} {
142+
const { effect, started } = migrationActor.send({ type: "START", operation });
135143

136-
log("=== Migration Complete ===");
137-
completeMigration({ members, articles, projects, images });
138-
} catch (e) {
139-
const errorMessage = e instanceof Error ? e.message : String(e);
140-
log(`=== Migration Failed: ${errorMessage} ===`);
141-
failMigration(errorMessage);
142-
} finally {
143-
// Cleanup
144-
if (repoPath) {
145-
log("Cleaning up temporary files...");
146-
await rm(repoPath, { recursive: true, force: true }).catch(() => {});
147-
}
144+
if (!started) {
145+
return { started: false, message: "Migration already in progress" };
148146
}
147+
148+
// Execute the effect (async work) in background
149+
executeEffect(effect);
150+
151+
const labels: Record<MigrationOperation, string> = {
152+
migrate: "Migration",
153+
cleanup: "Cleanup",
154+
delete: "Delete",
155+
};
156+
157+
return { started: true, message: `${labels[operation]} started` };
149158
}
159+
160+
// Convenience exports for backward compatibility
161+
export const startDataMigration = () => startOperation("migrate");
162+
export const startImageCleanup = () => startOperation("cleanup");
163+
export const startDeleteAll = () => startOperation("delete");

0 commit comments

Comments
 (0)