@@ -13,9 +13,10 @@ import {endHRTimeInMs, startHRTime} from '@shopify/cli-kit/node/hrtime'
13
13
import { performActionWithRetryAfterRecovery } from '@shopify/cli-kit/common/retry'
14
14
import { useConcurrentOutputContext } from '@shopify/cli-kit/node/ui/components'
15
15
import { JsonMapType } from '@shopify/cli-kit/node/toml'
16
- import { AbortError } from '@shopify/cli-kit/node/error'
17
16
import { isUnitTest } from '@shopify/cli-kit/node/context/local'
18
17
import { getArrayRejectingUndefined } from '@shopify/cli-kit/common/array'
18
+ import { AbortError } from '@shopify/cli-kit/node/error'
19
+ import { ClientError } from 'graphql-request'
19
20
import { Writable } from 'stream'
20
21
21
22
interface DevSessionOptions {
@@ -48,6 +49,12 @@ interface UserError {
48
49
category : string
49
50
}
50
51
52
+ interface DevSessionPayload {
53
+ shopFqdn : string
54
+ appId : string
55
+ assetsUrl : string
56
+ }
57
+
51
58
type DevSessionResult =
52
59
| { status : 'updated' | 'created' | 'aborted' }
53
60
| { status : 'remote-error' ; error : UserError [ ] }
@@ -65,6 +72,10 @@ export function devSessionStatus() {
65
72
}
66
73
}
67
74
75
+ export function resetDevSessionStatus ( ) {
76
+ isDevSessionReady = false
77
+ }
78
+
68
79
export async function setupDevSessionProcess ( {
69
80
app,
70
81
apiKey,
@@ -88,12 +99,7 @@ export const pushUpdatesForDevSession: DevProcessFunction<DevSessionOptions> = a
88
99
{ stderr, stdout, abortSignal : signal } ,
89
100
options ,
90
101
) => {
91
- const { developerPlatformClient, appWatcher} = options
92
-
93
- isDevSessionReady = false
94
- const refreshToken = async ( ) => {
95
- return developerPlatformClient . refreshToken ( )
96
- }
102
+ const { appWatcher} = options
97
103
98
104
const processOptions = { ...options , stderr, stdout, signal, bundlePath : appWatcher . buildOutputPath }
99
105
@@ -106,7 +112,7 @@ export const pushUpdatesForDevSession: DevProcessFunction<DevSessionOptions> = a
106
112
return
107
113
}
108
114
109
- // If there are any errors build errors, don't update the dev session
115
+ // If there are any build errors, don't update the dev session
110
116
const anyError = event . extensionEvents . some ( ( eve ) => eve . buildResult ?. status === 'error' )
111
117
if ( anyError ) return
112
118
@@ -133,19 +139,16 @@ export const pushUpdatesForDevSession: DevProcessFunction<DevSessionOptions> = a
133
139
} )
134
140
135
141
const networkStartTime = startHRTime ( )
136
- await performActionWithRetryAfterRecovery ( async ( ) => {
137
- const result = await bundleExtensionsAndUpload ( { ...processOptions , app : event . app } )
138
- await handleDevSessionResult ( result , { ...processOptions , app : event . app } , event )
139
- const endTime = endHRTimeInMs ( event . startTime )
140
- const endNetworkTime = endHRTimeInMs ( networkStartTime )
141
- outputDebug ( `✅ Event handled [Network: ${ endNetworkTime } ms -- Total: ${ endTime } ms]` , processOptions . stdout )
142
- } , refreshToken )
142
+ const result = await bundleExtensionsAndUpload ( { ...processOptions , app : event . app } )
143
+ await handleDevSessionResult ( result , { ...processOptions , app : event . app } , event )
144
+ outputDebug (
145
+ `✅ Event handled [Network: ${ endHRTimeInMs ( networkStartTime ) } ms - Total: ${ endHRTimeInMs ( event . startTime ) } ms]` ,
146
+ processOptions . stdout ,
147
+ )
143
148
} )
144
149
. onStart ( async ( event ) => {
145
- await performActionWithRetryAfterRecovery ( async ( ) => {
146
- const result = await bundleExtensionsAndUpload ( { ...processOptions , app : event . app } )
147
- await handleDevSessionResult ( result , { ...processOptions , app : event . app } )
148
- } , refreshToken )
150
+ const result = await bundleExtensionsAndUpload ( { ...processOptions , app : event . app } )
151
+ await handleDevSessionResult ( result , { ...processOptions , app : event . app } )
149
152
} )
150
153
}
151
154
@@ -168,7 +171,7 @@ async function handleDevSessionResult(
168
171
169
172
// If we failed to create a session, exit the process. Don't throw an error in tests as it can't be caught due to the
170
173
// async nature of the process.
171
- if ( ! isDevSessionReady && ! isUnitTest ( ) ) throw new AbortError ( 'Failed to create dev session' )
174
+ if ( ! isDevSessionReady && ! isUnitTest ( ) ) throw new AbortError ( 'Failed to start dev session. ' )
172
175
}
173
176
174
177
/**
@@ -227,11 +230,7 @@ async function bundleExtensionsAndUpload(options: DevSessionProcessOptions): Pro
227
230
228
231
// Get a signed URL to upload the zip file
229
232
if ( currentBundleController . signal . aborted ) return { status : 'aborted' }
230
- const signedURL = await getExtensionUploadURL ( options . developerPlatformClient , {
231
- apiKey : options . appId ,
232
- organizationId : options . organizationId ,
233
- id : options . appId ,
234
- } )
233
+ const signedURL = await getSignedURLWithRetry ( options )
235
234
236
235
// Upload the zip file
237
236
if ( currentBundleController . signal . aborted ) return { status : 'aborted' }
@@ -244,33 +243,67 @@ async function bundleExtensionsAndUpload(options: DevSessionProcessOptions): Pro
244
243
headers : form . getHeaders ( ) ,
245
244
} )
246
245
247
- const payload = { shopFqdn : options . storeFqdn , appId : options . appId , assetsUrl : signedURL }
246
+ const payload : DevSessionPayload = { shopFqdn : options . storeFqdn , appId : options . appId , assetsUrl : signedURL }
248
247
249
248
// Create or update the dev session
250
249
if ( currentBundleController . signal . aborted ) return { status : 'aborted' }
251
250
try {
252
251
if ( isDevSessionReady ) {
253
- const result = await options . developerPlatformClient . devSessionUpdate ( payload )
252
+ const result = await devSessionUpdateWithRetry ( payload , options . developerPlatformClient )
254
253
const errors = result . devSessionUpdate ?. userErrors ?? [ ]
255
254
if ( errors . length ) return { status : 'remote-error' , error : errors }
256
255
return { status : 'updated' }
257
256
} else {
258
- const result = await options . developerPlatformClient . devSessionCreate ( payload )
257
+ const result = await devSessionCreateWithRetry ( payload , options . developerPlatformClient )
259
258
const errors = result . devSessionCreate ?. userErrors ?? [ ]
260
259
if ( errors . length ) return { status : 'remote-error' , error : errors }
261
260
return { status : 'created' }
262
261
}
263
262
// eslint-disable-next-line @typescript-eslint/no-explicit-any
264
263
} catch ( error : any ) {
265
264
if ( error . statusCode === 401 ) {
266
- // Re-throw the error so the recovery procedure can be executed
267
265
throw new Error ( 'Unauthorized' )
266
+ } else if ( error instanceof ClientError ) {
267
+ if ( error . response . status === 401 || error . response . status === 403 ) {
268
+ throw new AbortError ( 'Auth session expired. Please run `shopify app dev` again.' )
269
+ } else {
270
+ outputDebug ( JSON . stringify ( error . response , null , 2 ) , options . stdout )
271
+ throw new AbortError ( 'Unknown error' )
272
+ }
268
273
} else {
269
274
return { status : 'unknown-error' , error}
270
275
}
271
276
}
272
277
}
273
278
279
+ async function getSignedURLWithRetry ( options : DevSessionProcessOptions ) {
280
+ return performActionWithRetryAfterRecovery (
281
+ async ( ) =>
282
+ getExtensionUploadURL ( options . developerPlatformClient , {
283
+ apiKey : options . appId ,
284
+ organizationId : options . organizationId ,
285
+ id : options . appId ,
286
+ } ) ,
287
+ ( ) => options . developerPlatformClient . refreshToken ( ) ,
288
+ )
289
+ }
290
+
291
+ async function devSessionUpdateWithRetry ( payload : DevSessionPayload , developerPlatformClient : DeveloperPlatformClient ) {
292
+ return performActionWithRetryAfterRecovery (
293
+ async ( ) => developerPlatformClient . devSessionUpdate ( payload ) ,
294
+ ( ) => developerPlatformClient . refreshToken ( ) ,
295
+ )
296
+ }
297
+
298
+ // If the Dev Session Create fails, we try to refresh the token and retry the operation
299
+ // This only happens if an error is thrown. Won't be triggered if we receive an error inside the response.
300
+ async function devSessionCreateWithRetry ( payload : DevSessionPayload , developerPlatformClient : DeveloperPlatformClient ) {
301
+ return performActionWithRetryAfterRecovery (
302
+ async ( ) => developerPlatformClient . devSessionCreate ( payload ) ,
303
+ ( ) => developerPlatformClient . refreshToken ( ) ,
304
+ )
305
+ }
306
+
274
307
async function processUserErrors (
275
308
errors : UserError [ ] | Error | string ,
276
309
options : DevSessionProcessOptions ,
0 commit comments