Skip to content

Commit d6a3909

Browse files
DevonLcolinmoynes
andauthored
fix: paginate repository list to load all workspace repositories (#25)
* fix: paginate repository list to load all workspace repositories - Add shared paginated fetcher in workspaceRepositoryFetcher.js - Fetch repos in page_size=500 chunks until all pages are consumed - Show withProgress loading indicator during multi-page fetches - Sort merged repository list alphabetically by name - Return error cleanly if the first page fails - Log warning and return partial results if a later page fails - Wire fetcher into workspaceNode.js and cloudsmithProvider.js - Support both normal workspace expansion and defaultWorkspace mode - Add regression tests for multi-page, first-page failure, partial failure, and default-workspace integration * fix: use API-reported page size for pagination stop check - Detect short final page from pagination.pageSize instead of requested size - Treat non-array response data as error instead of silent empty result --------- Co-authored-by: Colin Moynes <77994705+colinmoynes@users.noreply.github.com>
1 parent 6ae36fa commit d6a3909

6 files changed

Lines changed: 608 additions & 14 deletions

File tree

models/workspaceNode.js

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
const vscode = require("vscode");
44
const path = require("path");
55
const { CloudsmithAPI } = require("../util/cloudsmithAPI");
6+
const InfoNode = require("./infoNode");
67
const repositoryNode = require("./repositoryNode");
78
const { WorkspaceInfoNode } = require("./workspaceInfoNode");
9+
const workspaceRepositoryFetcher = require("../util/workspaceRepositoryFetcher");
810

911
class WorkspaceNode {
1012
constructor(item, context) {
@@ -32,22 +34,22 @@ class WorkspaceNode {
3234
async getRepositories() {
3335
const context = this.context;
3436
const workspace = this.workspace;
35-
const cloudsmithAPI = new CloudsmithAPI(context);
36-
const repositories = await cloudsmithAPI.get(
37-
"repos/" + workspace + "/?sort=name"
37+
const result = await workspaceRepositoryFetcher.fetchWorkspaceRepositories(
38+
context,
39+
workspace
3840
);
3941

40-
const RepositoryNodes = [];
41-
if (typeof repositories === "string") {
42-
vscode.window.showErrorMessage(
43-
`Could not load repositories for ${workspace}. ${repositories}`
44-
);
45-
return RepositoryNodes;
42+
if (result.error) {
43+
return [new InfoNode(
44+
"Failed to load repositories",
45+
"Check your connection and try refreshing",
46+
`Failed to load repositories for ${workspace}: ${result.error}`,
47+
"warning"
48+
)];
4649
}
4750

48-
if (!Array.isArray(repositories)) {
49-
return RepositoryNodes;
50-
}
51+
const repositories = result.repositories;
52+
const RepositoryNodes = [];
5153

5254
for (const repo of repositories) {
5355
const repositoryNodeInst = new repositoryNode(

test/cloudsmithProvider.test.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const assert = require("assert");
2+
const { CloudsmithAPI } = require("../util/cloudsmithAPI");
3+
const { ConnectionManager } = require("../util/connectionManager");
4+
const workspaceRepositoryFetcher = require("../util/workspaceRepositoryFetcher");
5+
const { CloudsmithProvider } = require("../views/cloudsmithProvider");
6+
7+
suite("CloudsmithProvider Test Suite", () => {
8+
let originalConnect;
9+
let originalGet;
10+
let originalFetchWorkspaceRepositories;
11+
let provider;
12+
let cacheUpdates;
13+
14+
const context = {
15+
globalState: {
16+
update(key, value) {
17+
cacheUpdates.push({ key, value });
18+
},
19+
},
20+
};
21+
22+
setup(() => {
23+
cacheUpdates = [];
24+
provider = new CloudsmithProvider(context);
25+
26+
originalConnect = ConnectionManager.prototype.connect;
27+
originalGet = CloudsmithAPI.prototype.get;
28+
originalFetchWorkspaceRepositories =
29+
workspaceRepositoryFetcher.fetchWorkspaceRepositories;
30+
31+
ConnectionManager.prototype.connect = async () => "true";
32+
CloudsmithAPI.prototype.get = async endpoint => {
33+
if (endpoint.startsWith("quota/")) {
34+
return {
35+
usage: {
36+
display: {
37+
storage: { used: "0 GB", plan_limit: "10 GB", percentage_used: "0" },
38+
bandwidth: { used: "0 GB", plan_limit: "10 GB", percentage_used: "0" },
39+
},
40+
},
41+
};
42+
}
43+
44+
return [];
45+
};
46+
});
47+
48+
teardown(() => {
49+
ConnectionManager.prototype.connect = originalConnect;
50+
CloudsmithAPI.prototype.get = originalGet;
51+
workspaceRepositoryFetcher.fetchWorkspaceRepositories =
52+
originalFetchWorkspaceRepositories;
53+
});
54+
55+
test("loads repositories for the default workspace through the shared fetcher", async () => {
56+
workspaceRepositoryFetcher.fetchWorkspaceRepositories = async () => ({
57+
repositories: [
58+
{ name: "repo-a", slug: "repo-a" },
59+
{ name: "repo-b", slug: "repo-b" },
60+
],
61+
error: null,
62+
warning: null,
63+
partial: false,
64+
});
65+
66+
const nodes = await provider.getRepositories("workspace-a");
67+
68+
assert.strictEqual(nodes.length, 3);
69+
assert.strictEqual(nodes[0].workspaceName, "workspace-a");
70+
assert.strictEqual(nodes[1].name, "repo-a");
71+
assert.strictEqual(nodes[2].name, "repo-b");
72+
assert.strictEqual(cacheUpdates.length, 1);
73+
assert.strictEqual(cacheUpdates[0].key, "CloudsmithCache");
74+
assert.deepStrictEqual(cacheUpdates[0].value.workspaces, [
75+
{ name: "workspace-a", slug: "workspace-a" },
76+
]);
77+
});
78+
});

test/workspaceNode.test.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const assert = require("assert");
2+
const { CloudsmithAPI } = require("../util/cloudsmithAPI");
3+
const workspaceRepositoryFetcher = require("../util/workspaceRepositoryFetcher");
4+
const WorkspaceNode = require("../models/workspaceNode");
5+
6+
suite("WorkspaceNode Test Suite", () => {
7+
let originalGet;
8+
let originalFetchWorkspaceRepositories;
9+
let cacheUpdates;
10+
let context;
11+
12+
setup(() => {
13+
originalGet = CloudsmithAPI.prototype.get;
14+
originalFetchWorkspaceRepositories =
15+
workspaceRepositoryFetcher.fetchWorkspaceRepositories;
16+
cacheUpdates = [];
17+
context = {
18+
globalState: {
19+
update(key, value) {
20+
cacheUpdates.push({ key, value });
21+
},
22+
},
23+
};
24+
25+
CloudsmithAPI.prototype.get = async () => "quota unavailable";
26+
});
27+
28+
teardown(() => {
29+
CloudsmithAPI.prototype.get = originalGet;
30+
workspaceRepositoryFetcher.fetchWorkspaceRepositories =
31+
originalFetchWorkspaceRepositories;
32+
});
33+
34+
test("shows an error child when the first repository page fails", async () => {
35+
workspaceRepositoryFetcher.fetchWorkspaceRepositories = async () => ({
36+
repositories: [],
37+
error: "Response status: 500",
38+
warning: null,
39+
partial: false,
40+
});
41+
42+
const node = new WorkspaceNode(
43+
{ name: "Workspace A", slug: "workspace-a" },
44+
context
45+
);
46+
47+
const children = await node.getChildren();
48+
49+
assert.strictEqual(children.length, 2);
50+
assert.strictEqual(children[1]._label, "Failed to load repositories");
51+
assert.strictEqual(cacheUpdates.length, 0);
52+
});
53+
54+
test("updates the repository cache with fetched repositories", async () => {
55+
workspaceRepositoryFetcher.fetchWorkspaceRepositories = async () => ({
56+
repositories: [
57+
{ name: "repo-a", slug: "repo-a" },
58+
{ name: "repo-b", slug: "repo-b" },
59+
],
60+
error: null,
61+
warning: null,
62+
partial: false,
63+
});
64+
65+
const node = new WorkspaceNode(
66+
{ name: "Workspace A", slug: "workspace-a" },
67+
context
68+
);
69+
70+
const repos = await node.getRepositories();
71+
72+
assert.strictEqual(repos.length, 2);
73+
assert.strictEqual(repos[0].name, "repo-a");
74+
assert.strictEqual(cacheUpdates.length, 1);
75+
assert.strictEqual(cacheUpdates[0].key, "CloudsmithCache");
76+
assert.deepStrictEqual(
77+
cacheUpdates[0].value.workspaces.map(repo => repo.slug),
78+
["repo-a", "repo-b"]
79+
);
80+
});
81+
});

0 commit comments

Comments
 (0)