dependencyiq 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/httpRetry.js CHANGED
@@ -1,48 +1,48 @@
1
- /**
2
- * Shared retry/backoff helper for the HTTP calls this project makes to
3
- * GitLab Orbit and package registries (npm, PyPI, Go proxy, Maven
4
- * Central). A flaky network hiccup during a live demo or a CI run
5
- * shouldn't read as "Orbit unavailable" / "registry lookup failed" when
6
- * a second attempt would have succeeded — but a real rejection (a 404 for
7
- * "this package doesn't exist," a 400 for a malformed query) must never
8
- * be retried, since retrying a deterministic rejection just wastes time
9
- * and risks masking a real bug as a transient one.
10
- *
11
- * Transient = no HTTP response at all (DNS failure, connection reset,
12
- * timeout) or a 5xx from the server. Anything else (4xx, or a 2xx that
13
- * the caller itself decides is wrong) is final on the first attempt.
14
- */
15
-
16
- function isTransientError(error) {
17
- const transientCodes = new Set(['ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN']);
18
- if (error.code && transientCodes.has(error.code)) return true;
19
- const status = error.response?.status;
20
- // No response at all (network-level failure axios didn't tag with a
21
- // known code) is treated the same as a 5xx: worth one retry.
22
- return status === undefined || status >= 500;
23
- }
24
-
25
- /**
26
- * @param {() => Promise<any>} fn - the call to attempt
27
- * @param {Object} [options]
28
- * @param {number} [options.retries=2] - additional attempts after the first
29
- * @param {number} [options.baseDelayMs=300] - delay before the first retry; doubles each subsequent attempt
30
- * @returns {Promise<any>}
31
- */
32
- async function withRetry(fn, { retries = 2, baseDelayMs = 300 } = {}) {
33
- let lastError;
34
- for (let attempt = 0; attempt <= retries; attempt += 1) {
35
- try {
36
- return await fn();
37
- } catch (error) {
38
- lastError = error;
39
- const isFinalAttempt = attempt === retries;
40
- if (isFinalAttempt || !isTransientError(error)) throw error;
41
- const delayMs = baseDelayMs * 2 ** attempt;
42
- await new Promise(resolve => { setTimeout(resolve, delayMs); });
43
- }
44
- }
45
- throw lastError;
46
- }
47
-
48
- module.exports = { withRetry, isTransientError };
1
+ /**
2
+ * Shared retry/backoff helper for the HTTP calls this project makes to
3
+ * GitLab Orbit and package registries (npm, PyPI, Go proxy, Maven
4
+ * Central). A flaky network hiccup during a live demo or a CI run
5
+ * shouldn't read as "Orbit unavailable" / "registry lookup failed" when
6
+ * a second attempt would have succeeded — but a real rejection (a 404 for
7
+ * "this package doesn't exist," a 400 for a malformed query) must never
8
+ * be retried, since retrying a deterministic rejection just wastes time
9
+ * and risks masking a real bug as a transient one.
10
+ *
11
+ * Transient = no HTTP response at all (DNS failure, connection reset,
12
+ * timeout) or a 5xx from the server. Anything else (4xx, or a 2xx that
13
+ * the caller itself decides is wrong) is final on the first attempt.
14
+ */
15
+
16
+ function isTransientError(error) {
17
+ const transientCodes = new Set(['ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN']);
18
+ if (error.code && transientCodes.has(error.code)) return true;
19
+ const status = error.response?.status;
20
+ // No response at all (network-level failure axios didn't tag with a
21
+ // known code) is treated the same as a 5xx: worth one retry.
22
+ return status === undefined || status >= 500;
23
+ }
24
+
25
+ /**
26
+ * @param {() => Promise<any>} fn - the call to attempt
27
+ * @param {Object} [options]
28
+ * @param {number} [options.retries=2] - additional attempts after the first
29
+ * @param {number} [options.baseDelayMs=300] - delay before the first retry; doubles each subsequent attempt
30
+ * @returns {Promise<any>}
31
+ */
32
+ async function withRetry(fn, { retries = 2, baseDelayMs = 300 } = {}) {
33
+ let lastError;
34
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
35
+ try {
36
+ return await fn();
37
+ } catch (error) {
38
+ lastError = error;
39
+ const isFinalAttempt = attempt === retries;
40
+ if (isFinalAttempt || !isTransientError(error)) throw error;
41
+ const delayMs = baseDelayMs * 2 ** attempt;
42
+ await new Promise(resolve => { setTimeout(resolve, delayMs); });
43
+ }
44
+ }
45
+ throw lastError;
46
+ }
47
+
48
+ module.exports = { withRetry, isTransientError };
@@ -204,6 +204,45 @@ async function findProjectsImportingPackage(groupId, packageName) {
204
204
  return Array.from(byProject.values());
205
205
  }
206
206
 
207
+ /**
208
+ * Does Orbit have indexed source for this project in the given language?
209
+ * Orbit indexes the DEFAULT branch only. A project whose default branch is,
210
+ * say, JavaScript will have File nodes — but scanning that project's Python
211
+ * feature branch must NOT be treated as "indexed", or a zero-importer Python
212
+ * package looks falsely "unused". So we ask specifically: does Orbit have any
213
+ * File of `language` for this project? If `language` is omitted we fall back
214
+ * to "any File at all". Returns false on any query error — the safe
215
+ * direction, since this only ever suppresses a removal recommendation, never
216
+ * invents one.
217
+ */
218
+ async function isProjectIndexed(projectId, language) {
219
+ try {
220
+ const fileToProjectEdges = await relationshipCandidates('File', 'Project', FILE_TO_PROJECT_EDGE_CANDIDATES);
221
+ const result = await queryWithRelationshipFallback(
222
+ (fileToProjectEdge) => ({
223
+ query_type: 'traversal',
224
+ nodes: [
225
+ { id: 'p', entity: 'Project', node_ids: [Number(projectId)] },
226
+ {
227
+ id: 'f',
228
+ entity: 'File',
229
+ columns: ['path', 'language'],
230
+ ...(language ? { filters: { language: { contains: language } } } : {}),
231
+ },
232
+ ],
233
+ relationships: [
234
+ { type: fileToProjectEdge, from: 'f', to: 'p' },
235
+ ],
236
+ limit: 1,
237
+ }),
238
+ [fileToProjectEdges]
239
+ );
240
+ return (result.result || []).length > 0;
241
+ } catch {
242
+ return false;
243
+ }
244
+ }
245
+
207
246
  module.exports = {
208
247
  getStatus,
209
248
  getSchema,
@@ -211,4 +250,5 @@ module.exports = {
211
250
  query,
212
251
  findPackageImporters,
213
252
  findProjectsImportingPackage,
253
+ isProjectIndexed,
214
254
  };