Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import * as Types from './types.js'
import {JsonMapType} from '@shopify/cli-kit/node/toml'

import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'

export type DevSessionHeartbeatMutationVariables = Types.Exact<{
appId: Types.Scalars['String']['input']
buildStatus?: Types.InputMaybe<Types.Scalars['String']['input']>
tunnelUrl?: Types.InputMaybe<Types.Scalars['String']['input']>
}>

export type DevSessionHeartbeatMutation = {
devSessionHeartbeat?: {
userErrors: {message: string; on: JsonMapType; field?: string[] | null; category: string}[]
} | null
}

export const DevSessionHeartbeat = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: {kind: 'Name', value: 'DevSessionHeartbeat'},
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}},
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'buildStatus'}},
type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'tunnelUrl'}},
type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'devSessionHeartbeat'},
arguments: [
{
kind: 'Argument',
name: {kind: 'Name', value: 'appId'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'buildStatus'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'buildStatus'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'tunnelUrl'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'tunnelUrl'}},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'userErrors'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: 'message'}},
{kind: 'Field', name: {kind: 'Name', value: 'on'}},
{kind: 'Field', name: {kind: 'Name', value: 'field'}},
{kind: 'Field', name: {kind: 'Name', value: 'category'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
],
},
},
],
} as unknown as DocumentNode<DevSessionHeartbeatMutation, DevSessionHeartbeatMutationVariables>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
mutation DevSessionHeartbeat($appId: String!, $buildStatus: String, $tunnelUrl: String) {
devSessionHeartbeat(appId: $appId, buildStatus: $buildStatus, tunnelUrl: $tunnelUrl) {
userErrors {
message
on
field
category
}
}
}
3 changes: 3 additions & 0 deletions packages/app/src/cli/models/app/app.test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
DeveloperPlatformClient,
DevSessionCreateOptions,
DevSessionDeleteOptions,
DevSessionHeartbeatOptions,
DevSessionUpdateOptions,
} from '../../utilities/developer-platform-client.js'
import {AllAppExtensionRegistrationsQuerySchema} from '../../api/graphql/all_app_extension_registrations.js'
Expand Down Expand Up @@ -1492,6 +1493,8 @@ export function testDeveloperPlatformClient(stubs: Partial<DeveloperPlatformClie
Promise.resolve(`https://test.shopify.com/${app.organizationId}/apps/${app.id}`),
devSessionCreate: (_input: DevSessionCreateOptions) => Promise.resolve({devSessionCreate: {userErrors: []}}),
devSessionUpdate: (_input: DevSessionUpdateOptions) => Promise.resolve({devSessionUpdate: {userErrors: []}}),
devSessionHeartbeat: (_input: DevSessionHeartbeatOptions) =>
Promise.resolve({devSessionHeartbeat: {userErrors: []}}),
devSessionDelete: (_input: DevSessionDeleteOptions) => Promise.resolve({devSessionDelete: {userErrors: []}}),
getCreateDevStoreLink: (org: Organization) =>
Promise.resolve(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
this.idEnvironmentVariableName = `SHOPIFY_${constantize(this.localIdentifier)}_ID`
this.outputPath = this.directory
this.uid = this.buildUIDFromStrategy()
this.devUUID = `dev-${this.uid}`
this.devUUID = this.uid

if (this.features.includes('esbuild') || this.type === 'tax_calculation') {
this.outputPath = joinPath(this.directory, 'dist', this.outputFileName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import {DevSessionProcessOptions} from './dev-session-process.js'
import {AppEvent, AppEventWatcher, ExtensionEvent} from '../../app-events/app-event-watcher.js'
import {compressBundle, getUploadURL, uploadToGCS, writeManifestToBundle} from '../../../bundle.js'
import {DevSessionCreateOptions, DevSessionUpdateOptions} from '../../../../utilities/developer-platform-client.js'
import {
DevSessionCreateOptions,
DevSessionHeartbeatOptions,
DevSessionUpdateOptions,
} from '../../../../utilities/developer-platform-client.js'
import {AppManifest} from '../../../../models/app/app.js'
import {endHRTimeInMs, startHRTime} from '@shopify/cli-kit/node/hrtime'
import {ClientError} from 'graphql-request'
Expand Down Expand Up @@ -41,6 +45,7 @@
private readonly bundlePath: string
private readonly appEventsProcessor: SerialBatchProcessor<AppEvent>
private failedEvents: AppEvent[] = []
private heartbeatInterval?: NodeJS.Timeout

private constructor(processOptions: DevSessionProcessOptions, stdout: Writable) {
this.statusManager = processOptions.devSessionStatusManager
Expand All @@ -51,6 +56,16 @@
this.appEventsProcessor = new SerialBatchProcessor((events: AppEvent[]) => this.processEvents(events))
}

/**
* Stop the heartbeat process
*/
public stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = undefined
}
}

private async start() {
await this.logger.info(`Preparing app preview on ${this.options.storeFqdn}`)
this.statusManager.setMessage('LOADING')
Expand Down Expand Up @@ -217,6 +232,7 @@
await this.logger.success(`✅ Ready, watching for changes in your app `)
await this.logger.logExtensionUpdateMessages(event)
this.statusManager.setMessage('READY')
this.startHeartbeat()
} else if (result.status === 'aborted') {
await this.logger.debug('❌ App preview update aborted (new change detected or error during update)')
} else if (result.status === 'remote-error' || result.status === 'unknown-error') {
Expand Down Expand Up @@ -389,6 +405,7 @@
* @param payload - The payload to update the dev session with
*/
private async devSessionUpdateWithRetry(payload: DevSessionUpdateOptions): Promise<DevSessionResult> {
console.log(JSON.stringify(payload.manifest, null, 2))

Check failure on line 408 in packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts#L408

[no-console] Unexpected console statement.
const result = await this.options.developerPlatformClient.devSessionUpdate(payload)
const errors = result.devSessionUpdate?.userErrors ?? []
if (errors.length) return {status: 'remote-error', error: errors}
Expand All @@ -407,4 +424,37 @@
if (errors.length) return {status: 'remote-error', error: errors}
return {status: 'created'}
}

/**
* Start the heartbeat process that sends periodic updates to the server
* The heartbeat runs every 3 seconds after the dev session is created
*/
private startHeartbeat() {
// Clear any existing interval
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
}

// Send heartbeat every 3 seconds
this.heartbeatInterval = setInterval(() => {
this.sendHeartbeat().catch(async (error) => {
await this.logger.debug(`Heartbeat error: ${error.message}`)
})
}, 2000)
}

/**
* Send a heartbeat to the server with the current status
*/
private async sendHeartbeat() {
const payload: DevSessionHeartbeatOptions = {
shopFqdn: this.options.storeFqdn,
appId: this.options.appId,
tunnelUrl: this.options.appLocalProxyURL,
buildStatus: this.statusManager.status.isReady ? 'ready' : 'building',
}

// await this.logger.info(`Sending heartbeat with payload: ${JSON.stringify(payload)}`)
await this.options.developerPlatformClient.devSessionHeartbeat(payload)
}
}
7 changes: 7 additions & 0 deletions packages/app/src/cli/utilities/developer-platform-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
} from '../api/graphql/partners/generated/update-draft.js'
import {DevSessionCreateMutation} from '../api/graphql/app-dev/generated/dev-session-create.js'
import {DevSessionUpdateMutation} from '../api/graphql/app-dev/generated/dev-session-update.js'
import {DevSessionHeartbeatMutation} from '../api/graphql/app-dev/generated/dev-session-heartbeat.js'
import {DevSessionDeleteMutation} from '../api/graphql/app-dev/generated/dev-session-delete.js'
import {AppLogsOptions} from '../services/app-logs/utils.js'
import {AppLogData} from '../services/app-logs/types.js'
Expand Down Expand Up @@ -166,6 +167,11 @@ export interface DevSessionUpdateOptions extends DevSessionSharedOptions {
inheritedModuleUids: string[]
}

export interface DevSessionHeartbeatOptions extends DevSessionSharedOptions {
buildStatus?: string
tunnelUrl?: string
}

export type DevSessionDeleteOptions = DevSessionSharedOptions

type WithUserErrors<T> = T & {
Expand Down Expand Up @@ -290,6 +296,7 @@ export interface DeveloperPlatformClient {
appDeepLink: (app: MinimalAppIdentifiers) => Promise<string>
devSessionCreate: (input: DevSessionCreateOptions) => Promise<DevSessionCreateMutation>
devSessionUpdate: (input: DevSessionUpdateOptions) => Promise<DevSessionUpdateMutation>
devSessionHeartbeat: (input: DevSessionHeartbeatOptions) => Promise<DevSessionHeartbeatMutation>
devSessionDelete: (input: DevSessionSharedOptions) => Promise<DevSessionDeleteMutation>
getCreateDevStoreLink: (org: Organization) => Promise<TokenItem>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
createUnauthorizedHandler,
DevSessionUpdateOptions,
DevSessionCreateOptions,
DevSessionHeartbeatOptions,
DevSessionDeleteOptions,
UserError,
} from '../developer-platform-client.js'
Expand Down Expand Up @@ -86,6 +87,10 @@ import {
DevSessionUpdateMutation,
DevSessionUpdateMutationVariables,
} from '../../api/graphql/app-dev/generated/dev-session-update.js'
import {
DevSessionHeartbeat,
DevSessionHeartbeatMutation,
} from '../../api/graphql/app-dev/generated/dev-session-heartbeat.js'
import {DevSessionDelete, DevSessionDeleteMutation} from '../../api/graphql/app-dev/generated/dev-session-delete.js'
import {
FetchDevStoreByDomain,
Expand Down Expand Up @@ -1028,6 +1033,20 @@ export class AppManagementClient implements DeveloperPlatformClient {
return this.appDevRequest({query: DevSessionUpdate, shopFqdn, variables})
}

async devSessionHeartbeat({
appId,
shopFqdn,
buildStatus,
tunnelUrl,
}: DevSessionHeartbeatOptions): Promise<DevSessionHeartbeatMutation> {
const appIdNumber = String(numberFromGid(appId))
return this.appDevRequest({
query: DevSessionHeartbeat,
shopFqdn,
variables: {appId: appIdNumber, buildStatus, tunnelUrl},
})
}

async devSessionDelete({appId, shopFqdn}: DevSessionDeleteOptions): Promise<DevSessionDeleteMutation> {
const appIdNumber = String(numberFromGid(appId))
return this.appDevRequest({query: DevSessionDelete, shopFqdn, variables: {appId: appIdNumber}})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,12 @@ export class PartnersClient implements DeveloperPlatformClient {
throw new Error('Unsupported operation')
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async devSessionHeartbeat(_input: unknown): Promise<any> {
// Dev Sessions are not supported in partners client.
return Promise.resolve()
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async devSessionDelete(_input: unknown): Promise<any> {
// Dev Sessions are not supported in partners client.
Expand Down
Loading