Skip to content

Commit 29776e9

Browse files
committed
Merge branch 'main' into dependabot-npm_and_yarn-packages-ui-extensions-server-kit-vite-6.3.6
2 parents ac57f1a + df23ee4 commit 29776e9

File tree

13 files changed

+1303
-988
lines changed

13 files changed

+1303
-988
lines changed

.changeset/healthy-coins-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/app': patch
3+
---
4+
5+
Fix concurrency issue with downloading Functions binaries

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
"liquidjs": "10.20.1",
7171
"node-fetch": "^3.3.2",
7272
"nx": "21.5.2",
73-
"oclif": "4.20.1",
73+
"oclif": "4.22.22",
7474
"octokit-plugin-create-pull-request": "^3.12.2",
7575
"pathe": "1.1.1",
7676
"pin-github-action": "^3.3.1",
@@ -100,7 +100,6 @@
100100
"resolutions": {
101101
"@types/react": "17.0.2",
102102
"vite": "6.3.6",
103-
"@oclif/core": "4.4.0",
104103
"whatwg-url": "14.0.0",
105104
"supports-hyperlinks": "3.1.0",
106105
"@graphql-tools/utils": "10.7.2",

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"dependencies": {
5050
"@graphql-typed-document-node/core": "3.2.0",
5151
"@luckycatfactory/esbuild-graphql-loader": "3.8.1",
52-
"@oclif/core": "4.4.0",
52+
"@oclif/core": "4.5.3",
5353
"@shopify/cli-kit": "3.84.0",
5454
"@shopify/function-runner": "4.1.1",
5555
"@shopify/plugin-cloudflare": "3.84.0",

packages/app/src/cli/services/function/binaries.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,26 @@ describe('javy', () => {
141141
expect(fetch).toHaveBeenCalledOnce()
142142
await expect(fileExists(javy.path)).resolves.toBeTruthy()
143143
})
144+
145+
test('handles concurrent downloads of Javy', async () => {
146+
// Given
147+
await removeFile(javy.path)
148+
await expect(fileExists(javy.path)).resolves.toBeFalsy()
149+
vi.mocked(fetch).mockResolvedValue(new Response(gzipSync('javy binary')))
150+
151+
// When
152+
await Promise.all([
153+
downloadBinary(javy),
154+
downloadBinary(javy),
155+
downloadBinary(javy),
156+
downloadBinary(javy),
157+
downloadBinary(javy),
158+
])
159+
160+
// Then
161+
expect(fetch).toHaveBeenCalledOnce()
162+
await expect(fileExists(javy.path)).resolves.toBeTruthy()
163+
})
144164
})
145165

146166
describe('javy-plugin', () => {

packages/app/src/cli/services/function/binaries.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,52 @@ export function trampolineBinary() {
203203
return _trampoline
204204
}
205205

206+
const downloadsInProgress = new Map<string, Promise<void>>()
207+
206208
export async function downloadBinary(bin: DownloadableBinary) {
207209
const isDownloaded = await fileExists(bin.path)
208210
if (isDownloaded) {
209211
return
210212
}
211213

214+
// Downloads cannot run concurrently with `exec` since the `moveFile`
215+
// operation is not atomic. It will delete the destination file if it exists
216+
// which will cause `exec` to break if it tries to execute the file before
217+
// the source file has been moved.
218+
// We prevent downloads from happening concurrently with `exec` by enforcing
219+
// that only one download happens for an executable. If we get here, we check
220+
// if a download is in progress, wait for it to finish, and return without
221+
// downloading again. If it's not in progress, then we start the download.
222+
const downloadPromise = downloadsInProgress.get(bin.path)
223+
if (downloadPromise) {
224+
await downloadPromise
225+
// Return now since we can assume it's downloaded. If it's not, an exception should've
226+
// been thrown before which will cause the user-level operation to fail anyway.
227+
return
228+
}
229+
230+
// Do not perform any `await`s until we've called `downloadsInProgress.set`.
231+
// Calling `await` before that can cause a different JS task to become active
232+
// and start a concurrent download for this binary.
233+
234+
// My mental model is `performDownload` without the `await` will run
235+
// synchronously until the first `await` in the function and then
236+
// immediately return the promise which we then immediately store. Since JS
237+
// doesn't have preemptive concurrency, we should be able to safely assume a
238+
// different task in the task queue will not run in between starting
239+
// `downloadFn` and the `set` operation on the following line.
240+
const downloadOp = performDownload(bin)
241+
downloadsInProgress.set(bin.path, downloadOp)
242+
// Ensure we clean the entry if there's a failure.
243+
try {
244+
// Wait for the download to finish
245+
await downloadOp
246+
} finally {
247+
downloadsInProgress.delete(bin.path)
248+
}
249+
}
250+
251+
async function performDownload(bin: DownloadableBinary) {
212252
const url = bin.downloadUrl(process.platform, process.arch)
213253
outputDebug(`Downloading ${bin.name} ${bin.version} from ${url} to ${bin.path}`)
214254
const dir = dirname(bin.path)

packages/cli-kit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
"@bugsnag/js": "7.25.0",
104104
"@graphql-typed-document-node/core": "3.2.0",
105105
"@iarna/toml": "2.2.5",
106-
"@oclif/core": "4.4.0",
106+
"@oclif/core": "4.5.3",
107107
"@opentelemetry/api": "1.9.0",
108108
"@opentelemetry/core": "1.30.0",
109109
"@opentelemetry/exporter-metrics-otlp-http": "0.57.0",

packages/cli/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@
106106
"global-agent": "3.0.0"
107107
},
108108
"devDependencies": {
109-
"@oclif/core": "4.4.0",
110-
"@oclif/plugin-commands": "4.1.26",
111-
"@oclif/plugin-plugins": "5.4.43",
109+
"@oclif/core": "4.5.3",
110+
"@oclif/plugin-commands": "4.1.33",
111+
"@oclif/plugin-plugins": "5.4.47",
112112
"@shopify/app": "3.84.0",
113113
"@shopify/cli-kit": "3.84.0",
114114
"@shopify/plugin-cloudflare": "3.84.0",

packages/cli/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export {hooks as PluginHook} from '@oclif/plugin-plugins'
3333
export {AppSensitiveMetadataHook, AppInitHook, AppPublicMetadataHook} from '@shopify/app'
3434
export {push, pull, fetchStoreThemes} from '@shopify/theme'
3535

36-
export const HydrogenInitHook = HydrogenHooks.init
36+
export const HydrogenInitHook: unknown = HydrogenHooks.init
3737

3838
// Setup global support for environment variable based proxy configuration.
3939
createGlobalProxyAgent({

packages/plugin-cloudflare/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
]
4747
},
4848
"dependencies": {
49-
"@oclif/core": "4.4.0",
49+
"@oclif/core": "4.5.3",
5050
"@shopify/cli-kit": "3.84.0"
5151
},
5252
"devDependencies": {

packages/plugin-did-you-mean/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
]
4242
},
4343
"dependencies": {
44-
"@oclif/core": "4.4.0",
44+
"@oclif/core": "4.5.3",
4545
"@shopify/cli-kit": "3.84.0",
4646
"n-gram": "2.0.2"
4747
},

0 commit comments

Comments
 (0)