fix: try fetch the sketch by path if not in the cache

The sketch cache might be empty, when trying to generate
the secrets include in the main sketch file from the
`secrets` property.

Closes #1999

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2023-04-11 10:06:12 +02:00 committed by Akos Kitta
parent 5540170341
commit 54f210d4de
3 changed files with 102 additions and 5 deletions

View File

@ -57,6 +57,30 @@ export class CreateApi {
return result; return result;
} }
/**
* `sketchPath` is not the POSIX path but the path with the user UUID, username, etc.
* See [Create.Resource#path](./typings.ts). If `cache` is `true` and a sketch exists with the path,
* the cache will be updated with the new state of the sketch.
*/
// TODO: no nulls in API
async sketchByPath(
sketchPath: string,
cache = false
): Promise<Create.Sketch | null> {
const url = new URL(`${this.domain()}/sketches/byPath/${sketchPath}`);
const headers = await this.headers();
const sketch = await this.run<Create.Sketch>(url, {
method: 'GET',
headers,
});
if (sketch && cache) {
this.sketchCache.addSketch(sketch);
const posixPath = createPaths.toPosixPath(sketch.path);
this.sketchCache.purgeByPath(posixPath);
}
return sketch;
}
async sketches(limit = 50): Promise<Create.Sketch[]> { async sketches(limit = 50): Promise<Create.Sketch[]> {
const url = new URL(`${this.domain()}/sketches`); const url = new URL(`${this.domain()}/sketches`);
url.searchParams.set('user_id', 'me'); url.searchParams.set('user_id', 'me');
@ -86,7 +110,11 @@ export class CreateApi {
async createSketch( async createSketch(
posixPath: string, posixPath: string,
contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent() contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent(),
payloadOverride: Record<
string,
string | boolean | number | Record<string, unknown>
> = {}
): Promise<Create.Sketch> { ): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches`); const url = new URL(`${this.domain()}/sketches`);
const [headers, content] = await Promise.all([ const [headers, content] = await Promise.all([
@ -97,6 +125,7 @@ export class CreateApi {
ino: btoa(content), ino: btoa(content),
path: posixPath, path: posixPath,
user_id: 'me', user_id: 'me',
...payloadOverride,
}; };
const init = { const init = {
method: 'PUT', method: 'PUT',
@ -212,7 +241,17 @@ export class CreateApi {
return data; return data;
} }
const sketch = this.sketchCache.getSketch(createPaths.parentPosix(path)); const posixPath = createPaths.parentPosix(path);
let sketch = this.sketchCache.getSketch(posixPath);
// Workaround for https://github.com/arduino/arduino-ide/issues/1999.
if (!sketch) {
// Convert the ordinary sketch POSIX path to the Create path.
// For example, `/sketch_apr6a` will be transformed to `8a694e4b83878cc53472bd75ee928053:kittaakos/sketches_v2/sketch_apr6a`.
const createPathPrefix = this.sketchCache.createPathPrefix;
if (createPathPrefix) {
sketch = await this.sketchByPath(createPathPrefix + posixPath, true);
}
}
if ( if (
sketch && sketch &&
@ -448,13 +487,18 @@ export class CreateApi {
await this.run(url, init, ResponseResultProvider.NOOP); await this.run(url, init, ResponseResultProvider.NOOP);
} }
private fetchCounter = 0;
private async run<T>( private async run<T>(
requestInfo: URL, requestInfo: URL,
init: RequestInit | undefined, init: RequestInit | undefined,
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
): Promise<T> { ): Promise<T> {
console.debug(`HTTP ${init?.method}: ${requestInfo.toString()}`); const fetchCount = `[${++this.fetchCounter}]`;
const fetchStart = performance.now();
const method = init?.method ? `${init.method}: ` : '';
const url = requestInfo.toString();
const response = await fetch(requestInfo.toString(), init); const response = await fetch(requestInfo.toString(), init);
const fetchEnd = performance.now();
if (!response.ok) { if (!response.ok) {
let details: string | undefined = undefined; let details: string | undefined = undefined;
try { try {
@ -465,7 +509,18 @@ export class CreateApi {
const { statusText, status } = response; const { statusText, status } = response;
throw new CreateError(statusText, status, details); throw new CreateError(statusText, status, details);
} }
const parseStart = performance.now();
const result = await resultProvider(response); const result = await resultProvider(response);
const parseEnd = performance.now();
console.debug(
`HTTP ${fetchCount} ${method} ${url} [fetch: ${(
fetchEnd - fetchStart
).toFixed(2)} ms, parse: ${(parseEnd - parseStart).toFixed(
2
)} ms] body: ${
typeof result === 'string' ? result : JSON.stringify(result)
}`
);
return result; return result;
} }

View File

@ -1,12 +1,13 @@
import { FileStat } from '@theia/filesystem/lib/common/files'; import { FileStat } from '@theia/filesystem/lib/common/files';
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import { toPosixPath } from '../../create/create-paths'; import { splitSketchPath } from '../../create/create-paths';
import { Create } from '../../create/typings'; import { Create } from '../../create/typings';
@injectable() @injectable()
export class SketchCache { export class SketchCache {
sketches: Record<string, Create.Sketch> = {}; sketches: Record<string, Create.Sketch> = {};
fileStats: Record<string, FileStat> = {}; fileStats: Record<string, FileStat> = {};
private _createPathPrefix: string | undefined;
init(): void { init(): void {
// reset the data // reset the data
@ -32,7 +33,10 @@ export class SketchCache {
addSketch(sketch: Create.Sketch): void { addSketch(sketch: Create.Sketch): void {
const { path } = sketch; const { path } = sketch;
const posixPath = toPosixPath(path); const [pathPrefix, posixPath] = splitSketchPath(path);
if (pathPrefix !== this._createPathPrefix) {
this._createPathPrefix = pathPrefix;
}
this.sketches[posixPath] = sketch; this.sketches[posixPath] = sketch;
} }
@ -40,6 +44,10 @@ export class SketchCache {
return this.sketches[path] || null; return this.sketches[path] || null;
} }
get createPathPrefix(): string | undefined {
return this._createPathPrefix;
}
toString(): string { toString(): string {
return JSON.stringify({ return JSON.stringify({
sketches: this.sketches, sketches: this.sketches,

View File

@ -1,6 +1,7 @@
import { Container, ContainerModule } from '@theia/core/shared/inversify'; import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { assert, expect } from 'chai'; import { assert, expect } from 'chai';
import fetch from 'cross-fetch'; import fetch from 'cross-fetch';
import { posix } from 'path';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { ArduinoPreferences } from '../../browser/arduino-preferences'; import { ArduinoPreferences } from '../../browser/arduino-preferences';
import { AuthenticationClientService } from '../../browser/auth/authentication-client-service'; import { AuthenticationClientService } from '../../browser/auth/authentication-client-service';
@ -251,6 +252,39 @@ describe('create-api', () => {
expect(sketch).to.be.undefined; expect(sketch).to.be.undefined;
}); });
}); });
it("should fetch the sketch when transforming the 'secrets' into '#include' and the sketch is not in the cache", async () => {
const name = v4();
const posixPath = toPosix(name);
const newSketch = await createApi.createSketch(
posixPath,
'void setup(){} void loop(){}',
{
secrets: {
data: [
{
name: 'SECRET_THING',
value: '❤︎',
},
],
},
}
);
expect(newSketch).to.be.not.undefined;
expect(newSketch.secrets).to.be.not.undefined;
expect(Array.isArray(newSketch.secrets)).to.be.true;
expect(newSketch.secrets?.length).to.be.equal(1);
expect(newSketch.secrets?.[0]).to.be.deep.equal({
name: 'SECRET_THING',
value: '❤︎',
});
createApi.sketchCache.init(); // invalidate the cache
const content = await createApi.readFile(
posix.join(posixPath, `${name}.ino`)
);
expect(content.includes(`#include "${Create.arduino_secrets_file}"`)).to.be
.true;
});
}); });
// Using environment variables is recommended for testing but you can modify the module too. // Using environment variables is recommended for testing but you can modify the module too.