11import { PrismaClient } from '@prisma/client' ;
2- import { SeedProcess } from './seed-process.js' ;
3- import ora from 'ora' ;
2+ import { SeedProcess , GLOBAL_SEED } from './seed-process.js' ;
3+ import { Twisters } from 'twisters' ;
4+
5+ type AnyProcess = SeedProcess < any , any > ;
6+
7+ const DEFAULT_MAX_CONCURRENCY = 4 ;
8+
9+ const deriveSeed = ( baseSeed : number , processName : string ) : number => {
10+ let hash = baseSeed >>> 0 ;
11+ for ( let i = 0 ; i < processName . length ; i ++ ) {
12+ hash = ( Math . imul ( hash , 31 ) + processName . charCodeAt ( i ) ) >>> 0 ;
13+ }
14+ return hash ;
15+ } ;
416
517export class SeedRunner {
6- private instances : SeedProcess < any , any > [ ] = [ ] ;
18+ private instances : AnyProcess [ ] = [ ] ;
719 private prisma ! : PrismaClient ;
20+ private baseSeed = GLOBAL_SEED ;
21+ private maxConcurrency = DEFAULT_MAX_CONCURRENCY ;
822
923 withPrisma ( prisma : PrismaClient ) {
1024 this . prisma = prisma ;
1125 return this ;
1226 }
1327
14- register ( ...processes : SeedProcess < any , any > [ ] ) {
28+ withSeed ( seed : number ) {
29+ this . baseSeed = seed ;
30+ return this ;
31+ }
32+
33+ withMaxConcurrency ( max : number ) {
34+ this . maxConcurrency = Math . max ( 1 , max ) ;
35+ return this ;
36+ }
37+
38+ register ( ...processes : AnyProcess [ ] ) {
1539 this . instances . push ( ...processes ) ;
1640 return this ;
1741 }
1842
1943 async run ( ) {
2044 if ( ! this . prisma ) throw new Error ( 'SeedRunner requires a PrismaClient. Call withPrisma() before run().' ) ;
2145
22- const outputs = new Map < string , any > ( ) ;
23- const context : Record < string , any > = { } ;
2446 const total = this . instances . length ;
47+ const byName = new Map < string , AnyProcess > ( ) ;
48+ for ( const instance of this . instances ) {
49+ const { name } = instance . constructor ;
50+ if ( byName . has ( name ) ) throw new Error ( `Duplicate process registered: ${ name } ` ) ;
51+ byName . set ( name , instance ) ;
52+ }
53+
54+ const dependencyNames = new Map < string , string [ ] > ( ) ;
55+ for ( const instance of this . instances ) {
56+ const { name } = instance . constructor ;
57+ const deps = instance . dependencies ( ) . map ( ( depClass ) => depClass . name ) ;
58+ for ( const dep of deps ) {
59+ if ( ! byName . has ( dep ) ) {
60+ throw new Error ( `Process ${ name } depends on ${ dep } , which was not registered.` ) ;
61+ }
62+ }
63+ dependencyNames . set ( name , deps ) ;
64+ }
65+
66+ this . assertNoCycles ( dependencyNames ) ;
67+
2568 const maxNameLength = Math . max ( ...this . instances . map ( ( i ) => i . constructor . name . length ) ) ;
69+ const outputs = new Map < string , any > ( ) ;
70+ const context : Record < string , any > = { } ;
2671
2772 const mergeOutputs = ( target : Record < string , any > , source : Record < string , any > , sourceName : string ) => {
2873 const duplicateKeys = Object . keys ( source ) . filter ( ( key ) => key in target ) ;
@@ -32,53 +77,123 @@ export class SeedRunner {
3277 return Object . assign ( target , source ) ;
3378 } ;
3479
35- const totalStart = Date . now ( ) ;
80+ const remaining = new Set ( byName . keys ( ) ) ;
81+ const inFlight = new Map < string , Promise < void > > ( ) ;
82+ const completed = new Set < string > ( ) ;
83+ const startTimes = new Map < string , number > ( ) ;
84+ let launchedCount = 0 ;
85+ let failed : unknown = null ;
3686
87+ const totalStart = Date . now ( ) ;
88+ const twisters = new Twisters ( ) ;
3789 console . log ( ) ;
38- console . log ( ` 🌱 Starting seed — ${ total } processes\n` ) ;
90+ console . log ( ` 🌱 Starting seed — ${ total } processes (max ${ this . maxConcurrency } concurrent)\n` ) ;
91+
92+ const depsSatisfied = ( name : string ) => ( dependencyNames . get ( name ) ?? [ ] ) . every ( ( dep ) => completed . has ( dep ) ) ;
93+
94+ const launch = ( name : string ) => {
95+ const instance = byName . get ( name ) ! ;
96+ remaining . delete ( name ) ;
97+ launchedCount += 1 ;
98+ const index = `[${ String ( launchedCount ) . padStart ( String ( total ) . length ) } /${ total } ]` ;
99+ const label = instance . constructor . name . padEnd ( maxNameLength ) ;
100+
101+ twisters . put ( name , { text : ` ${ index } ${ label } ` } ) ;
102+ startTimes . set ( name , Date . now ( ) ) ;
39103
40- for ( let i = 0 ; i < this . instances . length ; i ++ ) {
41- const instance = this . instances [ i ] ;
42104 instance . prisma = this . prisma ;
43- const start = Date . now ( ) ;
44- const index = `[${ String ( i + 1 ) . padStart ( String ( total ) . length ) } /${ total } ]` ;
45- const name = instance . constructor . name . padEnd ( maxNameLength ) ;
46-
47- const spinner = ora ( {
48- text : `${ index } ${ name } ` ,
49- color : 'cyan'
50- } ) . start ( ) ;
51-
52- try {
53- const depOutputs = instance . dependencies ( ) . reduce < Record < string , any > > ( ( acc , depClass ) => {
54- const output = outputs . get ( depClass . name ) ;
55- if ( ! output ) throw new Error ( `Missing output for dependency: ${ depClass . name } ` ) ;
56- return mergeOutputs ( acc , output , depClass . name ) ;
57- } , { } ) ;
105+ instance . reseed ( deriveSeed ( this . baseSeed , name ) ) ;
58106
107+ const depOutputs = ( dependencyNames . get ( name ) ?? [ ] ) . reduce < Record < string , any > > ( ( acc , depName ) => {
108+ const output = outputs . get ( depName ) ;
109+ if ( ! output ) throw new Error ( `Missing output for dependency: ${ depName } ` ) ;
110+ return mergeOutputs ( acc , output , depName ) ;
111+ } , { } ) ;
112+
113+ const task = ( async ( ) => {
59114 const output = await instance . run ( depOutputs ) ;
115+ outputs . set ( name , output ) ;
116+ mergeOutputs ( context , output , name ) ;
117+ } ) ( )
118+ . then ( ( ) => {
119+ const elapsed = `${ ( ( Date . now ( ) - startTimes . get ( name ) ! ) / 1000 ) . toFixed ( 2 ) } s` ;
120+ twisters . put ( name , { active : false , text : ` ✔ ${ index } ${ label } ${ elapsed } ` } ) ;
121+ completed . add ( name ) ;
122+ } )
123+ . catch ( ( e ) => {
124+ const elapsed = `${ ( ( Date . now ( ) - startTimes . get ( name ) ! ) / 1000 ) . toFixed ( 2 ) } s` ;
125+ twisters . put ( name , { active : false , text : ` ✖ ${ index } ${ label } ${ elapsed } ` } ) ;
126+ if ( ! failed ) failed = e ;
127+ } )
128+ . finally ( ( ) => {
129+ inFlight . delete ( name ) ;
130+ } ) ;
131+
132+ inFlight . set ( name , task ) ;
133+ } ;
134+
135+ while ( ( remaining . size > 0 || inFlight . size > 0 ) && ! failed ) {
136+ const ready = [ ...remaining ] . filter ( depsSatisfied ) ;
60137
61- outputs . set ( instance . constructor . name , output ) ;
62- mergeOutputs ( context , output , instance . constructor . name ) ;
63-
64- const elapsed = `${ ( ( Date . now ( ) - start ) / 1000 ) . toFixed ( 2 ) } s` ;
65- spinner . succeed ( `${ index } ${ name } ${ elapsed } ` ) ;
66- } catch ( e ) {
67- const elapsed = `${ ( ( Date . now ( ) - start ) / 1000 ) . toFixed ( 2 ) } s` ;
68- spinner . fail ( `${ index } ${ name } ${ elapsed } ` ) ;
69- const totalElapsed = `${ ( ( Date . now ( ) - totalStart ) / 1000 ) . toFixed ( 2 ) } s` ;
70- console . log ( ) ;
71- console . log ( ` ❌ Seed failed on ${ instance . constructor . name . trim ( ) } failure(s) in ${ totalElapsed } ` ) ;
72- console . log ( ) ;
73- throw e ;
138+ while ( ready . length > 0 && inFlight . size < this . maxConcurrency ) {
139+ launch ( ready . shift ( ) ! ) ;
140+ }
141+
142+ if ( inFlight . size === 0 && remaining . size > 0 ) {
143+ break ;
144+ }
145+
146+ if ( inFlight . size > 0 ) {
147+ await Promise . race ( inFlight . values ( ) ) ;
74148 }
75149 }
76150
151+ await Promise . allSettled ( inFlight . values ( ) ) ;
152+
153+ twisters . forEachMessage ( ( _message , messageName ) => twisters . remove ( messageName ) ) ;
154+ twisters . flush ( ) ;
155+
156+ if ( failed ) {
157+ const totalElapsed = `${ ( ( Date . now ( ) - totalStart ) / 1000 ) . toFixed ( 2 ) } s` ;
158+ console . log ( ) ;
159+ console . log ( ` ❌ Seed failed in ${ totalElapsed } ` ) ;
160+ console . log ( ) ;
161+ throw failed ;
162+ }
163+
164+ if ( completed . size < total ) {
165+ const stuck = [ ...byName . keys ( ) ] . filter ( ( name ) => ! completed . has ( name ) ) ;
166+ throw new Error ( `Seed could not complete. Unreachable processes (dependency issue): ${ stuck . join ( ', ' ) } ` ) ;
167+ }
168+
77169 const totalElapsed = `${ ( ( Date . now ( ) - totalStart ) / 1000 ) . toFixed ( 2 ) } s` ;
78170 console . log ( ) ;
79171 console . log ( ` ✅ Seed complete — ${ total } processes finished in ${ totalElapsed } ` ) ;
80172 console . log ( ) ;
81173
82174 return context ;
83175 }
176+
177+ private assertNoCycles ( dependencyNames : Map < string , string [ ] > ) {
178+ const visiting = new Set < string > ( ) ;
179+ const visited = new Set < string > ( ) ;
180+ const stack : string [ ] = [ ] ;
181+
182+ const visit = ( name : string ) => {
183+ if ( visited . has ( name ) ) return ;
184+ if ( visiting . has ( name ) ) {
185+ const cycleStart = stack . indexOf ( name ) ;
186+ const cycle = [ ...stack . slice ( cycleStart ) , name ] . join ( ' → ' ) ;
187+ throw new Error ( `Dependency cycle detected: ${ cycle } ` ) ;
188+ }
189+ visiting . add ( name ) ;
190+ stack . push ( name ) ;
191+ for ( const dep of dependencyNames . get ( name ) ?? [ ] ) visit ( dep ) ;
192+ stack . pop ( ) ;
193+ visiting . delete ( name ) ;
194+ visited . add ( name ) ;
195+ } ;
196+
197+ for ( const name of dependencyNames . keys ( ) ) visit ( name ) ;
198+ }
84199}
0 commit comments