1
1
import { actions , afterMount , connect , kea , listeners , path , reducers , selectors } from 'kea'
2
-
3
2
import { loaders } from 'kea-loaders'
4
3
import { FrameScene , FrameType } from '../types'
5
4
import { socketLogic } from '../scenes/socketLogic'
6
-
7
5
import type { framesModelType } from './framesModelType'
8
6
import { router } from 'kea-router'
9
7
import { sanitizeScene } from '../scenes/frame/frameLogic'
10
8
import { apiFetch } from '../utils/apiFetch'
11
9
10
+ export interface FrameImageInfo {
11
+ url : string
12
+ expiresAt : number
13
+ }
14
+
12
15
export const framesModel = kea < framesModelType > ( [
13
16
connect ( { logic : [ socketLogic ] } ) ,
14
17
path ( [ 'src' , 'models' , 'framesModel' ] ) ,
@@ -18,8 +21,10 @@ export const framesModel = kea<framesModelType>([
18
21
redeployFrame : ( id : number ) => ( { id } ) ,
19
22
restartFrame : ( id : number ) => ( { id } ) ,
20
23
renderFrame : ( id : number ) => ( { id } ) ,
21
- updateFrameImage : ( id : number ) => ( { id } ) ,
24
+ updateFrameImage : ( id : number , force = true ) => ( { id, force } ) ,
22
25
deleteFrame : ( id : number ) => ( { id } ) ,
26
+ setFrameImageInfo : ( id : number , imageInfo : FrameImageInfo ) => ( { id, imageInfo } ) ,
27
+ updateFrameImageTimestamp : ( id : number ) => ( { id } ) ,
23
28
} ) ,
24
29
loaders ( ( { values } ) => ( {
25
30
frames : [
@@ -29,13 +34,16 @@ export const framesModel = kea<framesModelType>([
29
34
try {
30
35
const response = await apiFetch ( `/api/frames/${ id } ` )
31
36
if ( ! response . ok ) {
32
- throw new Error ( 'Failed to fetch logs ' )
37
+ throw new Error ( 'Failed to fetch frame ' )
33
38
}
34
39
const data = await response . json ( )
35
40
const frame = data . frame as FrameType
36
41
return {
37
42
...values . frames ,
38
- frame : { ...frame , scenes : frame . scenes ?. map ( ( scene ) => sanitizeScene ( scene as FrameScene , frame ) ) } ,
43
+ [ frame . id ] : {
44
+ ...frame ,
45
+ scenes : frame . scenes ?. map ( ( scene ) => sanitizeScene ( scene as FrameScene , frame ) ) ,
46
+ } ,
39
47
}
40
48
} catch ( error ) {
41
49
console . error ( error )
@@ -49,7 +57,16 @@ export const framesModel = kea<framesModelType>([
49
57
throw new Error ( 'Failed to fetch frames' )
50
58
}
51
59
const data = await response . json ( )
52
- return Object . fromEntries ( ( data . frames as FrameType [ ] ) . map ( ( frame ) => [ frame . id , frame ] ) )
60
+ const framesDict = Object . fromEntries (
61
+ ( data . frames as FrameType [ ] ) . map ( ( frame ) => [
62
+ frame . id ,
63
+ {
64
+ ...frame ,
65
+ scenes : frame . scenes ?. map ( ( scene ) => sanitizeScene ( scene as FrameScene , frame ) ) ,
66
+ } ,
67
+ ] )
68
+ )
69
+ return framesDict
53
70
} catch ( error ) {
54
71
console . error ( error )
55
72
return values . frames
@@ -60,7 +77,7 @@ export const framesModel = kea<framesModelType>([
60
77
} ) ) ,
61
78
reducers ( ( ) => ( {
62
79
frames : [
63
- { } as Record < string , FrameType > ,
80
+ { } as Record < number , FrameType > ,
64
81
{
65
82
[ socketLogic . actionTypes . newFrame ] : ( state , { frame } ) => ( { ...state , [ frame . id ] : frame } ) ,
66
83
[ socketLogic . actionTypes . updateFrame ] : ( state , { frame } ) => ( { ...state , [ frame . id ] : frame } ) ,
@@ -71,11 +88,20 @@ export const framesModel = kea<framesModelType>([
71
88
} ,
72
89
} ,
73
90
] ,
91
+ frameImageInfos : [
92
+ { } as Record < number , FrameImageInfo > ,
93
+ {
94
+ setFrameImageInfo : ( state , { id, imageInfo } ) => ( { ...state , [ id ] : imageInfo } ) ,
95
+ } ,
96
+ ] ,
74
97
frameImageTimestamps : [
75
- { } as Record < string , number > ,
98
+ { } as Record < number , number > ,
76
99
{
77
- updateFrameImage : ( state , { id } ) =>
78
- state [ id ] === Math . floor ( Date . now ( ) / 1000 ) ? state : { ...state , [ id ] : Math . floor ( Date . now ( ) / 1000 ) } ,
100
+ updateFrameImageTimestamp : ( state , { id } ) => {
101
+ const nowSeconds = Math . floor ( Date . now ( ) / 1000 )
102
+ // Only update if it's different, to ensure a re-render
103
+ return state [ id ] === nowSeconds ? state : { ...state , [ id ] : nowSeconds }
104
+ } ,
79
105
} ,
80
106
] ,
81
107
} ) ) ,
@@ -88,18 +114,31 @@ export const framesModel = kea<framesModelType>([
88
114
) as FrameType [ ] ,
89
115
] ,
90
116
getFrameImage : [
91
- ( s ) => [ s . frameImageTimestamps ] ,
92
- ( frameImageTimestamps ) => {
93
- return ( id ) => {
94
- return `/api/frames/${ id } /image?t=${ frameImageTimestamps [ id ] ?? - 1 } `
117
+ ( s ) => [ s . frameImageInfos , s . frameImageTimestamps ] ,
118
+ ( frameImageInfos , frameImageTimestamps ) => {
119
+ return ( id : number ) => {
120
+ const info = frameImageInfos [ id ]
121
+ if ( ! info ) {
122
+ return null
123
+ }
124
+
125
+ const now = Math . floor ( Date . now ( ) / 1000 )
126
+ if ( ! info . expiresAt || now >= info . expiresAt ) {
127
+ // URL expired or invalid
128
+ return null
129
+ }
130
+
131
+ const timestamp = frameImageTimestamps [ id ] ?? - 1
132
+ // Append timestamp as a cache-buster
133
+ return `${ info . url } ${ info . url . includes ( '?' ) ? '&' : '?' } t=${ timestamp } `
95
134
}
96
135
} ,
97
136
] ,
98
137
} ) ,
99
138
afterMount ( ( { actions } ) => {
100
139
actions . loadFrames ( )
101
140
} ) ,
102
- listeners ( ( { props , actions } ) => ( {
141
+ listeners ( ( { actions , values } ) => ( {
103
142
renderFrame : async ( { id } ) => {
104
143
await apiFetch ( `/api/frames/${ id } /event/render` , { method : 'POST' } )
105
144
} ,
@@ -115,6 +154,33 @@ export const framesModel = kea<framesModelType>([
115
154
router . actions . push ( '/' )
116
155
}
117
156
} ,
157
+ updateFrameImage : async ( { id, force } ) => {
158
+ // Check if we have a valid URL
159
+ const imageUrl = values . getFrameImage ( id )
160
+ if ( imageUrl ) {
161
+ // The URL is still valid, no need to refetch new signed URL
162
+ // Just update timestamp to refresh (force reload)
163
+ if ( force ) {
164
+ actions . updateFrameImageTimestamp ( id )
165
+ }
166
+ return
167
+ }
168
+
169
+ // Need a new signed URL
170
+ const resp = await apiFetch ( `/api/frames/${ id } /image_link` )
171
+ if ( resp . ok ) {
172
+ const data = await resp . json ( )
173
+ const expiresAt = Math . floor ( Date . now ( ) / 1000 ) + data . expires_in
174
+ const imageInfo : FrameImageInfo = { url : data . url , expiresAt }
175
+ actions . setFrameImageInfo ( id , imageInfo )
176
+ // Update timestamp to ensure a new request even if the URL is same
177
+ if ( force ) {
178
+ actions . updateFrameImageTimestamp ( id )
179
+ }
180
+ } else {
181
+ console . error ( 'Failed to get image link for frame' , id )
182
+ }
183
+ } ,
118
184
[ socketLogic . actionTypes . newLog ] : ( { log } ) => {
119
185
if ( log . type === 'webhook' ) {
120
186
const parsed = JSON . parse ( log . line )
0 commit comments