@@ -16,6 +16,7 @@ import { FieldUpdateTrigger } from './common';
16
16
// frame storage by job id
17
17
const frameDataCache : Record < string , {
18
18
meta : FramesMetaData ;
19
+ metaFetchedTimestamp : number ;
19
20
chunkSize : number ;
20
21
mode : 'annotation' | 'interpolation' ;
21
22
startFrame : number ;
@@ -57,6 +58,7 @@ export class FramesMetaData {
57
58
public stopFrame : number ;
58
59
public frameStep : number ;
59
60
public chunkCount : number ;
61
+ public chunksUpdatedDate : string ;
60
62
61
63
#updateTrigger: FieldUpdateTrigger ;
62
64
@@ -71,6 +73,7 @@ export class FramesMetaData {
71
73
size : undefined ,
72
74
start_frame : undefined ,
73
75
stop_frame : undefined ,
76
+ chunks_updated_date : undefined ,
74
77
} ;
75
78
76
79
this . #updateTrigger = new FieldUpdateTrigger ( ) ;
@@ -149,6 +152,9 @@ export class FramesMetaData {
149
152
frameStep : {
150
153
get : ( ) => frameStep ,
151
154
} ,
155
+ chunksUpdatedDate : {
156
+ get : ( ) => data . chunks_updated_date ,
157
+ } ,
152
158
} ) ,
153
159
) ;
154
160
@@ -592,13 +598,45 @@ function getFrameMeta(jobID, frame): SerializedFramesMetaData['frames'][0] {
592
598
return frameMeta ;
593
599
}
594
600
601
+ async function refreshJobCacheIfOutdated ( jobID : number ) : Promise < void > {
602
+ const cached = frameDataCache [ jobID ] ;
603
+ if ( ! cached ) {
604
+ throw new Error ( 'Frame data cache is abscent' ) ;
605
+ }
606
+
607
+ const META_DATA_RELOAD_PERIOD = 1 * 60 * 60 * 1000 ; // 1 hour
608
+ const isOutdated = ( Date . now ( ) - cached . metaFetchedTimestamp ) > META_DATA_RELOAD_PERIOD ;
609
+
610
+ if ( isOutdated ) {
611
+ // get metadata again if outdated
612
+ const meta = await getFramesMeta ( 'job' , jobID , true ) ;
613
+ if ( new Date ( meta . chunksUpdatedDate ) > new Date ( cached . meta . chunksUpdatedDate ) ) {
614
+ // chunks were re-defined. Existing data not relevant anymore
615
+ // currently we only re-write meta, remove all cached frames from provider and clear cached context images
616
+ // other parameters (e.g. chunkSize) are not supposed to be changed
617
+ cached . meta = meta ;
618
+ cached . provider . cleanup ( Number . MAX_SAFE_INTEGER ) ;
619
+ for ( const frame of Object . keys ( cached . contextCache ) ) {
620
+ for ( const image of Object . values ( cached . contextCache [ + frame ] . data ) ) {
621
+ // close images to immediate memory release
622
+ image . close ( ) ;
623
+ }
624
+ }
625
+ cached . contextCache = { } ;
626
+ }
627
+
628
+ cached . metaFetchedTimestamp = Date . now ( ) ;
629
+ }
630
+ }
631
+
595
632
export function getContextImage ( jobID : number , frame : number ) : Promise < Record < string , ImageBitmap > > {
596
633
return new Promise < Record < string , ImageBitmap > > ( ( resolve , reject ) => {
597
634
if ( ! ( jobID in frameDataCache ) ) {
598
635
reject ( new Error (
599
636
'Frame data was not initialized for this job. Try first requesting any frame.' ,
600
637
) ) ;
601
638
}
639
+
602
640
const frameData = frameDataCache [ jobID ] ;
603
641
const requestId = frame ;
604
642
const { startFrame } = frameData ;
@@ -695,7 +733,9 @@ export async function getFrame(
695
733
dimension : DimensionType ,
696
734
getChunk : ( chunkIndex : number , quality : ChunkQuality ) => Promise < ArrayBuffer > ,
697
735
) : Promise < FrameData > {
698
- if ( ! ( jobID in frameDataCache ) ) {
736
+ const dataCacheExists = jobID in frameDataCache ;
737
+
738
+ if ( ! dataCacheExists ) {
699
739
const blockType = chunkType === 'video' ? BlockType . MP4VIDEO : BlockType . ARCHIVE ;
700
740
const meta = await getFramesMeta ( 'job' , jobID ) ;
701
741
@@ -718,6 +758,7 @@ export async function getFrame(
718
758
719
759
frameDataCache [ jobID ] = {
720
760
meta,
761
+ metaFetchedTimestamp : Date . now ( ) ,
721
762
chunkSize,
722
763
mode,
723
764
startFrame,
@@ -743,6 +784,22 @@ export async function getFrame(
743
784
} ;
744
785
}
745
786
787
+ // basically the following functions may be affected if job cache is outdated
788
+ // - getFrame
789
+ // - getContextImage
790
+ // - getCachedChunks
791
+ // And from this idea we should call refreshJobCacheIfOutdated from each one
792
+ // Hovewer, following from the order, these methods are usually called
793
+ // it may lead to even more confusing behaviour
794
+ //
795
+ // Usually user first receives frame, then user receives ranges and finally user receives context images
796
+ // In this case (extremely rare, but nevertheless possible) user may get context images related to another frame
797
+ // - if cache gets outdated after getFrame() call
798
+ // - and before getContextImage() call
799
+ // - and both calls refer to the same frame that is refreshed honeypot frame and this frame has context images
800
+ // Thus, it is better to only call `refreshJobCacheIfOutdated` from getFrame()
801
+ await refreshJobCacheIfOutdated ( jobID ) ;
802
+
746
803
const frameMeta = getFrameMeta ( jobID , frame ) ;
747
804
frameDataCache [ jobID ] . provider . setRenderSize ( frameMeta . width , frameMeta . height ) ;
748
805
frameDataCache [ jobID ] . decodeForward = isPlaying ;
@@ -759,7 +816,7 @@ export async function getFrame(
759
816
} ) ;
760
817
}
761
818
762
- export async function getDeletedFrames ( instanceType : 'job' | 'task' , id ) : Promise < Record < number , boolean > > {
819
+ export async function getDeletedFrames ( instanceType : 'job' | 'task' , id : number ) : Promise < Record < number , boolean > > {
763
820
if ( instanceType === 'job' ) {
764
821
const { meta } = frameDataCache [ id ] ;
765
822
return meta . deletedFrames ;
0 commit comments