Skip to content

Commit 5c0719d

Browse files
author
Akos Kitta
committed
fix: reduced unnecessary GET /sketches request
Ref: #1849 Signed-off-by: Akos Kitta <[email protected]>
1 parent eb1f247 commit 5c0719d

File tree

2 files changed

+150
-21
lines changed

2 files changed

+150
-21
lines changed

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

+16-17
Original file line numberDiff line numberDiff line change
@@ -62,26 +62,25 @@ export class CreateApi {
6262
url.searchParams.set('user_id', 'me');
6363
url.searchParams.set('limit', limit.toString());
6464
const headers = await this.headers();
65-
const result: { sketches: Create.Sketch[] } = { sketches: [] };
66-
67-
let partialSketches: Create.Sketch[] = [];
65+
const allSketches: Create.Sketch[] = [];
6866
let currentOffset = 0;
69-
do {
67+
while (true) {
7068
url.searchParams.set('offset', currentOffset.toString());
71-
partialSketches = (
72-
await this.run<{ sketches: Create.Sketch[] }>(url, {
73-
method: 'GET',
74-
headers,
75-
})
76-
).sketches;
77-
if (partialSketches.length !== 0) {
78-
result.sketches = result.sketches.concat(partialSketches);
69+
const { sketches } = await this.run<{ sketches: Create.Sketch[] }>(url, {
70+
method: 'GET',
71+
headers,
72+
});
73+
allSketches.push(...sketches);
74+
if (sketches.length < limit) {
75+
break;
7976
}
80-
currentOffset = currentOffset + limit;
81-
} while (partialSketches.length !== 0);
82-
83-
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
84-
return result.sketches;
77+
currentOffset += limit;
78+
// The create API doc show that there is `next` and `prev` pages, but it does not work
79+
// https://api2.arduino.cc/create/docs#!/sketches95v2/sketches_v2_search
80+
// IF sketchCount mod limit === 0, an extra fetch must happen to detect the end of the pagination.
81+
}
82+
allSketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
83+
return allSketches;
8584
}
8685

8786
async createSketch(

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

+134-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 { v4 } from 'uuid';
@@ -18,13 +22,14 @@ import queryString = require('query-string');
1822
const timeout = 60 * 1_000;
1923

2024
describe('create-api', () => {
21-
let createApi: CreateApi;
25+
let createApi: TestCreateApi;
2226

2327
before(async function () {
2428
this.timeout(timeout);
2529
try {
2630
const accessToken = await login();
27-
createApi = createContainer(accessToken).get<CreateApi>(CreateApi);
31+
createApi =
32+
createContainer(accessToken).get<TestCreateApi>(TestCreateApi);
2833
} catch (err) {
2934
if (err instanceof LoginFailed) {
3035
return this.skip();
@@ -42,7 +47,7 @@ describe('create-api', () => {
4247
const container = new Container({ defaultScope: 'Singleton' });
4348
container.load(
4449
new ContainerModule((bind) => {
45-
bind(CreateApi).toSelf().inSingletonScope();
50+
bind(TestCreateApi).toSelf().inSingletonScope();
4651
bind(SketchCache).toSelf().inSingletonScope();
4752
bind(AuthenticationClientService).toConstantValue(<
4853
AuthenticationClientService
@@ -223,6 +228,90 @@ describe('create-api', () => {
223228
expect(findByName(otherName, sketches)).to.be.not.undefined;
224229
});
225230

231+
it('should not run unnecessary fetches when retrieving all sketches', async () => {
232+
const content = 'void setup(){} void loop(){}';
233+
const maxLimit = 50; // https://github.com/arduino/arduino-ide/pull/875
234+
const sketchNames = [...Array(maxLimit - 1).keys()].map(() => v4());
235+
236+
// #region - sketch count < offset
237+
await sketchNames
238+
.map((name) => createApi.createSketch(toPosix(name), content))
239+
.reduce(async (acc, curr) => {
240+
await acc;
241+
return curr;
242+
}, Promise.resolve() as Promise<unknown>);
243+
244+
createApi.resetRequestRecording();
245+
let sketches = await createApi.sketches();
246+
let actualRecording = createApi.requestRecording.slice();
247+
248+
expect(sketches.length).to.be.equal(maxLimit - 1);
249+
sketchNames.forEach(
250+
(name) => expect(findByName(name, sketches)).to.be.not.undefined
251+
);
252+
253+
expect(actualRecording.length).to.be.equal(1);
254+
let getSketchesRequests = actualRecording.filter(
255+
(description) =>
256+
description.method === 'GET' &&
257+
description.pathname === '/create/v2/sketches' &&
258+
description.query &&
259+
description.query.includes(`limit=${maxLimit}`)
260+
);
261+
expect(getSketchesRequests.length).to.be.equal(1);
262+
// #endregion
263+
264+
// #region - sketch count === offset
265+
let extraName = v4();
266+
sketchNames.push(extraName);
267+
await createApi.createSketch(toPosix(extraName), content);
268+
269+
createApi.resetRequestRecording();
270+
sketches = await createApi.sketches();
271+
actualRecording = createApi.requestRecording.slice();
272+
273+
expect(sketches.length).to.be.equal(maxLimit);
274+
sketchNames.forEach(
275+
(name) => expect(findByName(name, sketches)).to.be.not.undefined
276+
);
277+
278+
expect(actualRecording.length).to.be.equal(2);
279+
getSketchesRequests = actualRecording.filter(
280+
(description) =>
281+
description.method === 'GET' &&
282+
description.pathname === '/create/v2/sketches' &&
283+
description.query &&
284+
description.query.includes(`limit=${maxLimit}`)
285+
);
286+
expect(getSketchesRequests.length).to.be.equal(2);
287+
// #endregion
288+
289+
// #region - sketch count > offset
290+
extraName = v4();
291+
sketchNames.push(extraName);
292+
await createApi.createSketch(toPosix(extraName), content);
293+
294+
createApi.resetRequestRecording();
295+
sketches = await createApi.sketches();
296+
actualRecording = createApi.requestRecording.slice();
297+
298+
expect(sketches.length).to.be.equal(maxLimit + 1);
299+
sketchNames.forEach(
300+
(name) => expect(findByName(name, sketches)).to.be.not.undefined
301+
);
302+
303+
expect(actualRecording.length).to.be.equal(2);
304+
getSketchesRequests = actualRecording.filter(
305+
(description) =>
306+
description.method === 'GET' &&
307+
description.pathname === '/create/v2/sketches' &&
308+
description.query &&
309+
description.query.includes(`limit=${maxLimit}`)
310+
);
311+
expect(getSketchesRequests.length).to.be.equal(2);
312+
// #endregion
313+
});
314+
226315
['.', '-', '_'].map((char) => {
227316
it(`should create a new sketch with '${char}' in the sketch folder name although it's disallowed from the Create Editor`, async () => {
228317
const name = `sketch${char}`;
@@ -298,3 +387,44 @@ class LoginFailed extends Error {
298387
Object.setPrototypeOf(this, LoginFailed.prototype);
299388
}
300389
}
390+
391+
@injectable()
392+
class TestCreateApi extends CreateApi {
393+
private _recording: RequestDescription[] = [];
394+
395+
constructor() {
396+
super();
397+
const originalRun = this['run'];
398+
this['run'] = (url, init, resultProvider) => {
399+
this._recording.push(createRequestDescription(url, init));
400+
return originalRun.bind(this)(url, init, resultProvider);
401+
};
402+
}
403+
404+
resetRequestRecording(): void {
405+
this._recording = [];
406+
}
407+
408+
get requestRecording(): RequestDescription[] {
409+
return this._recording;
410+
}
411+
}
412+
413+
interface RequestDescription {
414+
readonly origin: string;
415+
readonly pathname: string;
416+
readonly query?: string;
417+
418+
readonly method?: string | undefined;
419+
readonly serializedBody?: string | undefined;
420+
}
421+
422+
function createRequestDescription(
423+
url: URL,
424+
init?: RequestInit | undefined
425+
): RequestDescription {
426+
const { origin, pathname, search: query } = url;
427+
const method = init?.method;
428+
const serializedBody = init?.body?.toString();
429+
return { origin, pathname, query, method, serializedBody };
430+
}

0 commit comments

Comments
 (0)