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
89import { spawn } from "node:child_process" ;
910import { rm } from "node:fs/promises" ;
1011import { tmpdir } from "node:os" ;
1112import { 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" ;
1315import {
1416 cleanupInvalidImageUrls ,
1517 deleteAllMigratedData ,
@@ -21,7 +23,7 @@ import {
2123
2224const 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
3941async 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