@@ -11,7 +11,9 @@ import { recursiveDelete } from '../lib/recursive-delete'
1111import * as Log from './output/log'
1212
1313const FILE_BUILD_ID = 'HEAD_BUILD_ID'
14+ const FILE_UPDATED_AT = 'UPDATED_AT'
1415const DIR_FILES_NAME = 'files'
16+ const MAX_SHUTTLES = 4
1517
1618const mkdirp = promisify ( mkdirpModule )
1719const fsExists = promisify ( fs . exists )
@@ -29,6 +31,63 @@ type ChunkGraphManifest = {
2931 hashes : { [ page : string ] : string }
3032}
3133
34+ async function findCachedShuttles ( apexShuttleDirectory : string ) {
35+ return ( await Promise . all (
36+ await fsReadDir ( apexShuttleDirectory ) . then ( shuttleFiles =>
37+ shuttleFiles . map ( async f => ( {
38+ file : f ,
39+ stats : await fsLstat ( path . join ( apexShuttleDirectory , f ) ) ,
40+ } ) )
41+ )
42+ ) )
43+ . filter ( ( { stats } ) => stats . isDirectory ( ) )
44+ . map ( ( { file } ) => file )
45+ }
46+
47+ async function pruneShuttles ( apexShuttleDirectory : string ) {
48+ const allShuttles = await findCachedShuttles ( apexShuttleDirectory )
49+ if ( allShuttles . length <= MAX_SHUTTLES ) {
50+ return
51+ }
52+
53+ const datedShuttles : { updatedAt : Date ; shuttleDirectory : string } [ ] = [ ]
54+ for ( const shuttleId of allShuttles ) {
55+ const shuttleDirectory = path . join ( apexShuttleDirectory , shuttleId )
56+ const updatedAtPath = path . join ( shuttleDirectory , FILE_UPDATED_AT )
57+
58+ let updatedAt : Date
59+ try {
60+ updatedAt = new Date ( ( await fsReadFile ( updatedAtPath , 'utf8' ) ) . trim ( ) )
61+ } catch ( err ) {
62+ if ( err . code === 'ENOENT' ) {
63+ await recursiveDelete ( shuttleDirectory )
64+ continue
65+ }
66+ throw err
67+ }
68+
69+ datedShuttles . push ( { updatedAt, shuttleDirectory } )
70+ }
71+
72+ const sortedShuttles = datedShuttles . sort ( ( a , b ) =>
73+ Math . sign ( b . updatedAt . valueOf ( ) - a . updatedAt . valueOf ( ) )
74+ )
75+ let prunedShuttles = 0
76+ while ( sortedShuttles . length > MAX_SHUTTLES ) {
77+ const shuttleDirectory = sortedShuttles . pop ( )
78+ await recursiveDelete ( shuttleDirectory ! . shuttleDirectory )
79+ ++ prunedShuttles
80+ }
81+
82+ if ( prunedShuttles ) {
83+ Log . info (
84+ `decommissioned ${ prunedShuttles } old shuttle${
85+ prunedShuttles > 1 ? 's' : ''
86+ } `
87+ )
88+ }
89+ }
90+
3291function isShuttleValid ( {
3392 manifestPath,
3493 pagesDirectory,
@@ -111,20 +170,8 @@ export class FlyingShuttle {
111170 return path . join ( this . apexShuttleDirectory , this . flyingShuttleId )
112171 }
113172
114- private getShuttleIds = async ( ) =>
115- ( await Promise . all (
116- await fsReadDir ( this . apexShuttleDirectory ) . then ( shuttleFiles =>
117- shuttleFiles . map ( async f => ( {
118- file : f ,
119- stats : await fsLstat ( path . join ( this . apexShuttleDirectory , f ) ) ,
120- } ) )
121- )
122- ) )
123- . filter ( ( { stats } ) => stats . isDirectory ( ) )
124- . map ( ( { file } ) => file )
125-
126173 private findShuttleId = async ( ) => {
127- const shuttles = await this . getShuttleIds ( )
174+ const shuttles = await findCachedShuttles ( this . apexShuttleDirectory )
128175 return shuttles . find ( shuttleId => {
129176 try {
130177 const manifestPath = path . join (
@@ -364,6 +411,10 @@ export class FlyingShuttle {
364411 path . join ( this . shuttleDirectory , FILE_BUILD_ID ) ,
365412 this . buildId
366413 )
414+ await fsWriteFile (
415+ path . join ( this . shuttleDirectory , FILE_UPDATED_AT ) ,
416+ new Date ( ) . toISOString ( )
417+ )
367418
368419 const usedChunks = new Set ( )
369420 const pages = Object . keys ( storeManifest . pageChunks )
@@ -393,5 +444,11 @@ export class FlyingShuttle {
393444
394445 Log . info ( `flying shuttle payload: ${ usedChunks . size + 2 } files` )
395446 Log . ready ( 'flying shuttle docked' )
447+
448+ try {
449+ await pruneShuttles ( this . apexShuttleDirectory )
450+ } catch ( e ) {
451+ Log . error ( 'failed to prune old shuttles: ' + e )
452+ }
396453 }
397454}
0 commit comments