Skip to content

Commit 097c92d

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
fix: reduced unnecessary GET /sketches request
Ref: #1849 Signed-off-by: Akos Kitta <[email protected]>
1 parent b451e2d commit 097c92d

File tree

2 files changed

+107
-21
lines changed

2 files changed

+107
-21
lines changed

arduino-ide-extension/src/browser/create/create-api.ts

+16-17
Original file line numberDiff line numberDiff line change
@@ -86,26 +86,25 @@ export class CreateApi {
8686
url.searchParams.set('user_id', 'me');
8787
url.searchParams.set('limit', limit.toString());
8888
const headers = await this.headers();
89-
const result: { sketches: Create.Sketch[] } = { sketches: [] };
90-
91-
let partialSketches: Create.Sketch[] = [];
89+
const allSketches: Create.Sketch[] = [];
9290
let currentOffset = 0;
93-
do {
91+
while (true) {
9492
url.searchParams.set('offset', currentOffset.toString());
95-
partialSketches = (
96-
await this.run<{ sketches: Create.Sketch[] }>(url, {
97-
method: 'GET',
98-
headers,
99-
})
100-
).sketches;
101-
if (partialSketches.length !== 0) {
102-
result.sketches = result.sketches.concat(partialSketches);
93+
const { sketches } = await this.run<{ sketches: Create.Sketch[] }>(url, {
94+
method: 'GET',
95+
headers,
96+
});
97+
allSketches.push(...sketches);
98+
if (sketches.length < limit) {
99+
break;
103100
}
104-
currentOffset = currentOffset + limit;
105-
} while (partialSketches.length !== 0);
106-
107-
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
108-
return result.sketches;
101+
currentOffset += limit;
102+
// The create API doc show that there is `next` and `prev` pages, but it does not work
103+
// https://api2.arduino.cc/create/docs#!/sketches95v2/sketches_v2_search
104+
// IF sketchCount mod limit === 0, an extra fetch must happen to detect the end of the pagination.
105+
}
106+
allSketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
107+
return allSketches;
109108
}
110109

111110
async createSketch(

arduino-ide-extension/src/test/browser/create-api.test.ts

+91-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { Container, ContainerModule } from '@theia/core/shared/inversify';
1+
import {
2+
Container,
3+
ContainerModule,
4+
injectable,
5+
} from '@theia/core/shared/inversify';
26
import { assert, expect } from 'chai';
37
import fetch from 'cross-fetch';
48
import { posix } from 'path';
@@ -19,13 +23,14 @@ import queryString = require('query-string');
1923
const timeout = 60 * 1_000;
2024

2125
describe('create-api', () => {
22-
let createApi: CreateApi;
26+
let createApi: TestCreateApi;
2327

2428
before(async function () {
2529
this.timeout(timeout);
2630
try {
2731
const accessToken = await login();
28-
createApi = createContainer(accessToken).get<CreateApi>(CreateApi);
32+
createApi =
33+
createContainer(accessToken).get<TestCreateApi>(TestCreateApi);
2934
} catch (err) {
3035
if (err instanceof LoginFailed) {
3136
return this.skip();
@@ -43,7 +48,7 @@ describe('create-api', () => {
4348
const container = new Container({ defaultScope: 'Singleton' });
4449
container.load(
4550
new ContainerModule((bind) => {
46-
bind(CreateApi).toSelf().inSingletonScope();
51+
bind(TestCreateApi).toSelf().inSingletonScope();
4752
bind(SketchCache).toSelf().inSingletonScope();
4853
bind(AuthenticationClientService).toConstantValue(<
4954
AuthenticationClientService
@@ -224,6 +229,47 @@ describe('create-api', () => {
224229
expect(findByName(otherName, sketches)).to.be.not.undefined;
225230
});
226231

232+
[
233+
[-1, 1],
234+
[0, 2],
235+
[1, 2],
236+
].forEach(([diff, expected]) =>
237+
it(`should not run unnecessary fetches when retrieving all sketches (sketch count ${
238+
diff < 0 ? '<' : diff > 0 ? '>' : '='
239+
} limit)`, async () => {
240+
const content = 'void setup(){} void loop(){}';
241+
const maxLimit = 50; // https://github.com/arduino/arduino-ide/pull/875
242+
const sketchCount = maxLimit + diff;
243+
const sketchNames = [...Array(sketchCount).keys()].map(() => v4());
244+
245+
await sketchNames
246+
.map((name) => createApi.createSketch(toPosix(name), content))
247+
.reduce(async (acc, curr) => {
248+
await acc;
249+
return curr;
250+
}, Promise.resolve() as Promise<unknown>);
251+
252+
createApi.resetRequestRecording();
253+
const sketches = await createApi.sketches();
254+
const allRequests = createApi.requestRecording.slice();
255+
256+
expect(sketches.length).to.be.equal(sketchCount);
257+
sketchNames.forEach(
258+
(name) => expect(findByName(name, sketches)).to.be.not.undefined
259+
);
260+
261+
expect(allRequests.length).to.be.equal(expected);
262+
const getSketchesRequests = allRequests.filter(
263+
(description) =>
264+
description.method === 'GET' &&
265+
description.pathname === '/create/v2/sketches' &&
266+
description.query &&
267+
description.query.includes(`limit=${maxLimit}`)
268+
);
269+
expect(getSketchesRequests.length).to.be.equal(expected);
270+
})
271+
);
272+
227273
['.', '-', '_'].map((char) => {
228274
it(`should create a new sketch with '${char}' in the sketch folder name although it's disallowed from the Create Editor`, async () => {
229275
const name = `sketch${char}`;
@@ -332,3 +378,44 @@ class LoginFailed extends Error {
332378
Object.setPrototypeOf(this, LoginFailed.prototype);
333379
}
334380
}
381+
382+
@injectable()
383+
class TestCreateApi extends CreateApi {
384+
private _recording: RequestDescription[] = [];
385+
386+
constructor() {
387+
super();
388+
const originalRun = this['run'];
389+
this['run'] = (url, init, resultProvider) => {
390+
this._recording.push(createRequestDescription(url, init));
391+
return originalRun.bind(this)(url, init, resultProvider);
392+
};
393+
}
394+
395+
resetRequestRecording(): void {
396+
this._recording = [];
397+
}
398+
399+
get requestRecording(): RequestDescription[] {
400+
return this._recording;
401+
}
402+
}
403+
404+
interface RequestDescription {
405+
readonly origin: string;
406+
readonly pathname: string;
407+
readonly query?: string;
408+
409+
readonly method?: string | undefined;
410+
readonly serializedBody?: string | undefined;
411+
}
412+
413+
function createRequestDescription(
414+
url: URL,
415+
init?: RequestInit | undefined
416+
): RequestDescription {
417+
const { origin, pathname, search: query } = url;
418+
const method = init?.method;
419+
const serializedBody = init?.body?.toString();
420+
return { origin, pathname, query, method, serializedBody };
421+
}

0 commit comments

Comments
 (0)