From adc3baefa8c4c14927145165e9d2d48d23885241 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Wed, 8 Jan 2025 23:30:02 +0800 Subject: [PATCH 1/5] app: Include `default_version` by default This commit would ensure that `default_version` is included in the `crate` response by default. This eliminates the need to wait for the `versions` request to complete when `default_version` is the only version required. --- app/adapters/crate.js | 2 +- tests/adapters/crate-test.js | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/adapters/crate.js b/app/adapters/crate.js index 5dc3b05f122..8f22861eda7 100644 --- a/app/adapters/crate.js +++ b/app/adapters/crate.js @@ -37,7 +37,7 @@ export default class CrateAdapter extends ApplicationAdapter { function setDefaultInclude(query) { if (query.include === undefined) { // This ensures `crate.versions` are always fetched from another request. - query.include = 'keywords,categories,downloads'; + query.include = 'keywords,categories,downloads,default_version'; } return query; diff --git a/tests/adapters/crate-test.js b/tests/adapters/crate-test.js index 2dcf265ce20..a457ca2819f 100644 --- a/tests/adapters/crate-test.js +++ b/tests/adapters/crate-test.js @@ -23,20 +23,28 @@ module('Adapter | crate', function (hooks) { assert.strictEqual(bar?.name, 'bar'); }); - test('findRecord requests do not include versions by default', async function (assert) { - let _foo = this.server.create('crate', { name: 'foo' }); - let version = this.server.create('version', { crate: _foo }); + test('findRecord requests only include `default_version` by default', async function (assert) { + let crate = this.server.create('crate', { name: 'foo' }); + let versions = [ + this.server.create('version', { crate, num: '0.0.1' }), + this.server.create('version', { crate, num: '0.0.2' }), + this.server.create('version', { crate, num: '0.0.3' }), + ]; + let default_version = versions.find(v => v.num === '0.0.3'); let store = this.owner.lookup('service:store'); let foo = await store.findRecord('crate', 'foo'); assert.strictEqual(foo?.name, 'foo'); - // versions should not be loaded yet + // Only `defaul_version` should be loaded let versionsRef = foo.hasMany('versions'); - assert.deepEqual(versionsRef.ids(), []); + assert.deepEqual(versionsRef.ids(), [default_version.id]); await versionsRef.load(); - assert.deepEqual(versionsRef.ids(), [version.id]); + assert.deepEqual( + versionsRef.ids(), + versions.map(v => v.id), + ); }); }); From ac2acf0b6d27f1aa9c98b59259293a0ed6c3b528 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Wed, 8 Jan 2025 12:25:32 +0800 Subject: [PATCH 2/5] routes/crate/dependencies: Redirect directly to `default_version` --- app/routes/crate/dependencies.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/routes/crate/dependencies.js b/app/routes/crate/dependencies.js index 8ffa3c3aa8a..68334f99636 100644 --- a/app/routes/crate/dependencies.js +++ b/app/routes/crate/dependencies.js @@ -6,11 +6,8 @@ export default class VersionRoute extends Route { async model() { let crate = this.modelFor('crate'); - let versions = await crate.loadVersionsTask.perform(); - let { default_version } = crate; - let version = versions.find(version => version.num === default_version) ?? versions.lastObject; - this.router.replaceWith('crate.version-dependencies', crate, version.num); + this.router.replaceWith('crate.version-dependencies', crate, default_version); } } From b76b8a8965582a02f1a2860137b8e9a3b0baa57c Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Wed, 8 Jan 2025 12:42:00 +0800 Subject: [PATCH 3/5] adapters/version: Add `queryRecord` support for `GET /api/v1/crate/{name}/{version}` endpoint --- app/adapters/version.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/adapters/version.js b/app/adapters/version.js index c8be99bdd27..9e1819aad05 100644 --- a/app/adapters/version.js +++ b/app/adapters/version.js @@ -6,4 +6,12 @@ export default class VersionAdapter extends ApplicationAdapter { let num = snapshot.record.num; return `/${this.namespace}/crates/${crateName}/${num}`; } + + queryRecord(_store, _type, query) { + let { name, num } = query ?? {}; + let baseUrl = this.buildURL('crate', name); + let url = `${baseUrl}/${num}`; + let data = { ...query, name: undefined, num: undefined }; + return this.ajax(url, 'GET', { data }); + } } From 90e6bed7b255fe6e4278f40d3b624a45edd5d059 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Wed, 8 Jan 2025 12:44:50 +0800 Subject: [PATCH 4/5] models/crate: Add `loadedVersionsByNum` for lookup `versions` by `num` --- app/models/crate.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/crate.js b/app/models/crate.js index cc73c860eb9..67b520a0c9e 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -67,6 +67,14 @@ export default class Crate extends Model { return Object.fromEntries(versions.slice().map(v => [v.id, v])); } + /** @return {Map} */ + @cached + get loadedVersionsByNum() { + let versionsRef = this.hasMany('versions'); + let values = versionsRef.value(); + return new Map(values?.map(ref => [ref.num, ref])); + } + @cached get releaseTrackSet() { let map = new Map(); let { versionsObj: versions, versionIdsBySemver } = this; From 433abe73055f2b8e5b5511402715a4c7afc8aa7f Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Wed, 8 Jan 2025 20:23:18 +0800 Subject: [PATCH 5/5] routes/crate: Get or fetch requested version by `num` Previously, we fetched all versions and then found the requested version within them. This commit changes the approach to either retrieve the version from loaded versions or fetch it from the endpoint. This change allows us to migrate to paginated versions in the future. --- app/routes/crate/version-dependencies.js | 31 +++++++++++++++------ app/routes/crate/version.js | 25 +++++++++++++++-- e2e/acceptance/crate-dependencies.spec.ts | 2 +- tests/acceptance/crate-dependencies-test.js | 2 +- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/app/routes/crate/version-dependencies.js b/app/routes/crate/version-dependencies.js index 75068fd7af4..94f5999f262 100644 --- a/app/routes/crate/version-dependencies.js +++ b/app/routes/crate/version-dependencies.js @@ -2,21 +2,23 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; export default class VersionRoute extends Route { + @service store; @service router; async model(params, transition) { let crate = this.modelFor('crate'); - let versions; - try { - versions = await crate.loadVersionsTask.perform(); - } catch (error) { - let title = `${crate.name}: Failed to load version data`; - return this.router.replaceWith('catch-all', { transition, error, title, tryAgain: true }); - } - let requestedVersion = params.version_num; - let version = versions.find(version => version.num === requestedVersion); + let version = + crate.loadedVersionsByNum.get(requestedVersion) ?? + (await this.store + .queryRecord('version', { + name: crate.id, + num: requestedVersion, + }) + .catch(() => { + // ignored + })); if (!version) { let title = `${crate.name}: Version ${requestedVersion} not found`; return this.router.replaceWith('catch-all', { transition, title }); @@ -32,6 +34,17 @@ export default class VersionRoute extends Route { return version; } + async afterModel(_resolvedModel, transition) { + let crate = this.modelFor('crate'); + // TODO: Resolved version without waiting for versions to be resolved + try { + await crate.loadVersionsTask.perform(); + } catch (error) { + let title = `${crate.name}: Failed to load version data`; + return this.router.replaceWith('catch-all', { transition, error, title, tryAgain: true }); + } + } + setupController(controller, model) { controller.set('version', model); controller.set('crate', this.modelFor('crate')); diff --git a/app/routes/crate/version.js b/app/routes/crate/version.js index 683fbf8f3fd..d463978bea6 100644 --- a/app/routes/crate/version.js +++ b/app/routes/crate/version.js @@ -10,10 +10,12 @@ import { AjaxError } from '../../utils/ajax'; export default class VersionRoute extends Route { @service router; @service sentry; + @service store; async model(params, transition) { let crate = this.modelFor('crate'); + // TODO: Resolved version without waiting for versions to be resolved let versions; try { versions = await crate.loadVersionsTask.perform(); @@ -25,14 +27,33 @@ export default class VersionRoute extends Route { let version; let requestedVersion = params.version_num; if (requestedVersion) { - version = versions.find(version => version.num === requestedVersion); + version = + crate.loadedVersionsByNum.get(requestedVersion) ?? + (await this.store + .queryRecord('version', { + name: crate.id, + num: requestedVersion, + }) + .catch(() => { + // ignored + })); + if (!version) { let title = `${crate.name}: Version ${requestedVersion} not found`; return this.router.replaceWith('catch-all', { transition, title }); } } else { let { default_version } = crate; - version = versions.find(version => version.num === default_version); + version = + crate.loadedVersionsByNum.get(default_version) ?? + (await this.store + .queryRecord('version', { + name: crate.id, + num: default_version, + }) + .catch(() => { + // ignored + })); if (!version) { let versionNums = versions.map(it => it.num); diff --git a/e2e/acceptance/crate-dependencies.spec.ts b/e2e/acceptance/crate-dependencies.spec.ts index eb020ffa03f..65dd9870805 100644 --- a/e2e/acceptance/crate-dependencies.spec.ts +++ b/e2e/acceptance/crate-dependencies.spec.ts @@ -71,7 +71,7 @@ test.describe('Acceptance | crate dependencies page', { tag: '@acceptance' }, () test('shows an error page if versions fail to load', async ({ page, mirage, ember }) => { await mirage.addHook(server => { let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '2.0.0' }); + server.create('version', { crate, num: '1.0.0' }); server.get('/api/v1/crates/:crate_name/versions', {}, 500); }); diff --git a/tests/acceptance/crate-dependencies-test.js b/tests/acceptance/crate-dependencies-test.js index c009c89c223..859a5dc8bf4 100644 --- a/tests/acceptance/crate-dependencies-test.js +++ b/tests/acceptance/crate-dependencies-test.js @@ -74,7 +74,7 @@ module('Acceptance | crate dependencies page', function (hooks) { test('shows an error page if versions fail to load', async function (assert) { let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '2.0.0' }); + this.server.create('version', { crate, num: '1.0.0' }); this.server.get('/api/v1/crates/:crate_name/versions', {}, 500);