Skip to content

Commit

Permalink
fix: add retry logic for fetches, fix small data:// bug in fetches (#216
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Bubblyworld authored Feb 21, 2023
1 parent dea9d09 commit ba62529
Show file tree
Hide file tree
Showing 16 changed files with 242 additions and 181 deletions.
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"lit-element": "^2.5.1",
"mocha": "^9.2.2",
"open": "^8.4.1",
"prettier": "^2.8.4",
"rollup": "^2.79.1",
"typedoc": "^0.22.18"
},
Expand Down
29 changes: 29 additions & 0 deletions src/common/fetch-common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export interface Response {
status: number;
statusText?: string;
text?(): Promise<string>;
json?(): Promise<any>;
arrayBuffer?(): ArrayBuffer;
}

export type FetchFn = (
url: URL,
...args: any[]
) => Promise<Response | globalThis.Response>;

/**
* Wraps a fetch request with retry logic on exceptions, which is useful for
* spotty connections that may fail intermittently.
*/
export function wrapWithRetry(fetch: FetchFn): FetchFn {
return async function (url: URL, ...args: any[]) {
let retries = 0;
while (true) {
try {
return await fetch(url, ...args);
} catch (e) {
if (retries++ > 3) throw e;
}
}
};
}
51 changes: 12 additions & 39 deletions src/common/fetch-deno.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// @ts-ignore
import { fileURLToPath } from "url";
// @ts-ignore
// Caching disabled due to not respecting cache headers...
// import { cache } from "https://deno.land/x/cache/mod.ts";
import { wrapWithRetry, FetchFn } from "./fetch-common.js";

export function clearCache() {}

export const fetch = async function (url: URL, ...args: any[]) {
export const fetch: FetchFn = wrapWithRetry(async function (
url: URL,
...args: any[]
) {
const urlString = url.toString();
if (
urlString.startsWith("file:") ||
Expand All @@ -16,12 +16,14 @@ export const fetch = async function (url: URL, ...args: any[]) {
try {
let source: string;
if (urlString.startsWith("file:")) {
// @ts-ignore
source = await Deno.readTextFile(fileURLToPath(urlString));
// @ts-ignore - can only resolve Deno when running in Deno
source = (await Deno.readTextFile(fileURLToPath(urlString))) as string;
} else if (urlString.startsWith("node:")) {
source = "";
} else {
source = decodeURIComponent(urlString.slice(urlString.indexOf(",")));
source = decodeURIComponent(
urlString.slice(urlString.indexOf(",") + 1)
);
}
return {
status: 200,
Expand All @@ -32,7 +34,7 @@ export const fetch = async function (url: URL, ...args: any[]) {
return JSON.parse(source.toString());
},
arrayBuffer() {
return source;
return new TextEncoder().encode(source.toString()).buffer;
},
};
} catch (e) {
Expand All @@ -55,34 +57,5 @@ export const fetch = async function (url: URL, ...args: any[]) {
}
} else {
return globalThis.fetch(urlString, ...args);
// let file;
// try {
// file = await cache(urlString);
// }
// catch (e) {
// if (e.name === 'SyntaxError') {
// // Weird bug in Deno cache...
// // @ts-ignore
// return _fetch(url, ...args);
// }
// if (e.name === 'CacheError' && e.message === 'Not Found') {
// return { status: 404, statusText: e.toString() };
// }
// throw e;
// }
// @ts-ignore
// const source = await Deno.readTextFile(fromFileUrl(urlString));
// return {
// status: 200,
// async text () {
// return source.toString();
// },
// async json () {
// return JSON.parse(source.toString());
// },
// arrayBuffer () {
// return source;
// }
// };
}
};
});
7 changes: 5 additions & 2 deletions src/common/fetch-native.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FetchFn, wrapWithRetry } from "./fetch-common.js";

// Browser native fetch doesn't deal well with high contention
// restrict in-flight fetches to a pool of 100
let p = [];
Expand All @@ -10,7 +12,7 @@ function popFetchPool() {
if (p.length) p.shift()();
}

export async function fetch(url, opts) {
export const fetch: FetchFn = wrapWithRetry(async function fetch(url, opts) {
const poolQueue = pushFetchPool();
if (poolQueue) await poolQueue;
try {
Expand All @@ -35,5 +37,6 @@ export async function fetch(url, opts) {
} finally {
popFetchPool();
}
}
});

export const clearCache = () => {};
22 changes: 10 additions & 12 deletions src/common/fetch-node.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
// @ts-ignore
import version from "../version.js";
// @ts-ignore
import { wrapWithRetry, FetchFn } from "./fetch-common.js";
import path from "path";
// @ts-ignore
import { homedir } from "os";
// @ts-ignore
import process from "process";
// @ts-ignore
import rimraf from "rimraf";
// @ts-ignore
import makeFetchHappen from "make-fetch-happen";
// @ts-ignore
import { readFileSync } from "fs";
// @ts-ignore
import { Buffer } from "buffer";

let cacheDir: string;
Expand All @@ -38,7 +32,7 @@ const _fetch = makeFetchHappen.defaults({
headers: { "User-Agent": `jspm/generator@${version}` },
});

function sourceResponse(buffer) {
function sourceResponse(buffer: string | Buffer) {
return {
status: 200,
async text() {
Expand All @@ -48,7 +42,8 @@ function sourceResponse(buffer) {
return JSON.parse(buffer.toString());
},
arrayBuffer() {
return buffer.buffer || buffer;
if (buffer instanceof Buffer) return buffer.buffer;
return new TextEncoder().encode(buffer.toString()).buffer;
},
};
}
Expand All @@ -66,7 +61,10 @@ const dirResponse = {
},
};

export const fetch = async function (url: URL, opts?: Record<string, any>) {
export const fetch: FetchFn = wrapWithRetry(async function (
url: URL,
opts?: Record<string, any>
) {
if (!opts) throw new Error("Always expect fetch options to be passed");
const urlString = url.toString();
const protocol = urlString.slice(0, urlString.indexOf(":") + 1);
Expand Down Expand Up @@ -98,7 +96,7 @@ export const fetch = async function (url: URL, opts?: Record<string, any>) {
}
case "data:":
return sourceResponse(
decodeURIComponent(urlString.slice(urlString.indexOf(",")))
decodeURIComponent(urlString.slice(urlString.indexOf(",") + 1))
);
case "node:":
return sourceResponse("");
Expand All @@ -107,4 +105,4 @@ export const fetch = async function (url: URL, opts?: Record<string, any>) {
// @ts-ignore
return _fetch(url, opts);
}
};
});
8 changes: 6 additions & 2 deletions src/common/fetch-vscode.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FetchFn, wrapWithRetry } from "./fetch-common.js";
import { fetch as _fetch } from "./fetch-native.js";
export { clearCache } from "./fetch-native.js";

Expand Down Expand Up @@ -32,7 +33,10 @@ const dirResponse = {
// @ts-ignore
const vscode = require("vscode");

export const fetch = async function (url: URL, opts?: Record<string, any>) {
export const fetch: FetchFn = wrapWithRetry(async function (
url: URL,
opts?: Record<string, any>
) {
if (!opts) throw new Error("Always expect fetch options to be passed");
const urlString = url.toString();
const protocol = urlString.slice(0, urlString.indexOf(":") + 1);
Expand Down Expand Up @@ -71,4 +75,4 @@ export const fetch = async function (url: URL, opts?: Record<string, any>) {
// @ts-ignore
return _fetch(url, opts);
}
};
});
21 changes: 17 additions & 4 deletions src/install/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,10 @@ export class Installer {
}

if (pkgTarget instanceof URL) {
this.log("installer/installTarget", `${pkgName} ${pkgScope} -> ${pkgTarget.href} (URL)`);
this.log(
"installer/installTarget",
`${pkgName} ${pkgScope} -> ${pkgTarget.href} (URL)`
);
const installUrl = (pkgTarget.href +
(pkgTarget.href.endsWith("/") ? "" : "/")) as `${string}/`;
this.newInstalls = setResolution(
Expand Down Expand Up @@ -267,7 +270,9 @@ export class Installer {
if (pkg) {
this.log(
"installer/installTarget",
`${pkgName} ${pkgScope} -> ${JSON.stringify(latestPkg)} (existing match not latest)`
`${pkgName} ${pkgScope} -> ${JSON.stringify(
latestPkg
)} (existing match not latest)`
);
const installUrl = this.resolver.pkgToUrl(pkg, provider);
this.newInstalls = setResolution(
Expand Down Expand Up @@ -318,7 +323,10 @@ export class Installer {
traceSubpath: `./${string}` | ".",
parentUrl: string = this.installBaseUrl
): Promise<string | InstalledResolution> {
this.log("installer/install", `installing ${pkgName} from ${parentUrl} in scope ${pkgScope}`);
this.log(
"installer/install",
`installing ${pkgName} from ${parentUrl} in scope ${pkgScope}`
);
if (!this.installing) throwInternalError("Not installing");

// Anything installed in the scope of the installer's base URL is treated
Expand Down Expand Up @@ -348,7 +356,12 @@ export class Installer {
isTopLevel ? null : pkgScope
);
if (existingResolution) {
this.log("installer/install", `existing lock for ${pkgName} from ${parentUrl} in scope ${pkgScope} is ${JSON.stringify(existingResolution)}`);
this.log(
"installer/install",
`existing lock for ${pkgName} from ${parentUrl} in scope ${pkgScope} is ${JSON.stringify(
existingResolution
)}`
);
return existingResolution;
}

Expand Down
20 changes: 14 additions & 6 deletions src/install/lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ export interface LockResolutions {
}

interface PackageTargetMap {
[pkgName: string]: PackageTarget | URL,
[pkgName: string]: PackageTarget | URL;
}

export interface VersionConstraints {
primary: PackageTargetMap,
primary: PackageTargetMap;
secondary: {
[pkgUrl: `${string}/`]: PackageTargetMap,
}
[pkgUrl: `${string}/`]: PackageTargetMap;
};
}

export interface InstalledResolution {
Expand Down Expand Up @@ -321,14 +321,22 @@ export function getInstallsFor(
const installs: PackageInstall[] = [];
for (const alias of Object.keys(constraints.primary)) {
const target = constraints.primary[alias];
if (!(target instanceof URL) && target.registry === registry && target.name === name)
if (
!(target instanceof URL) &&
target.registry === registry &&
target.name === name
)
installs.push({ alias, pkgScope: null, ranges: target.ranges });
}
for (const pkgScope of Object.keys(constraints.secondary) as `${string}/`[]) {
const scope = constraints.secondary[pkgScope];
for (const alias of Object.keys(scope)) {
const target = scope[alias];
if (!(target instanceof URL) && target.registry === registry && target.name === name)
if (
!(target instanceof URL) &&
target.registry === registry &&
target.name === name
)
installs.push({ alias, pkgScope, ranges: target.ranges });
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/install/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ export async function parseTarget(
// package config if they exist:
const pcfg = await resolver.getPackageConfig(parentPkgUrl.href);
if (versionIndex === -1 && pcfg) {
const dep = pcfg.dependencies?.[alias] ||
const dep =
pcfg.dependencies?.[alias] ||
pcfg.peerDependencies?.[alias] ||
pcfg.optionalDependencies?.[alias] ||
pcfg.devDependencies?.[alias];
Expand All @@ -226,7 +227,7 @@ export async function parseTarget(
),
alias,
subpath: pkg.subpath,
}
};
}
}

Expand Down
4 changes: 1 addition & 3 deletions src/trace/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,7 @@ export class Resolver {
this.providers = Object.assign({}, this.providers, { [name]: provider });
}

parseUrlPkg(
url: string
): {
parseUrlPkg(url: string): {
pkg: ExactPackage;
subpath: null | `./${string}`;
source: { layer: string; provider: string };
Expand Down
Loading

0 comments on commit ba62529

Please sign in to comment.