chore(deps): update dependency eslint-plugin-unicorn to v70 (#86)

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
renovate[bot]
2026-07-07 12:08:40 +02:00
committed by GitHub
co-authored by Daniel Dietzler
parent ff27106981
commit 0f1e9c88d4
31 changed files with 142 additions and 125 deletions
+32 -30
View File
@@ -53,6 +53,37 @@ export class CloudflareRestClient implements ICloudflareRestClient {
private readonly fetchImpl?: typeof fetch, private readonly fetchImpl?: typeof fetch,
) {} ) {}
private async listPaginated<T>(path: string): Promise<T[]> {
const all: T[] = [];
let page = 1;
const separator = path.includes('?') ? '&' : '?';
const doFetch = this.fetchImpl ?? globalThis.fetch;
for (;;) {
const url = `${this.baseUrl}${path}${separator}page=${page}&per_page=${DEFAULT_PAGE_SIZE}`;
const response = await doFetch(url, {
headers: { Authorization: `Bearer ${this.apiToken}` },
});
if (!response.ok) {
// eslint-disable-next-line unicorn/prefer-await
const body = await response.text().catch(() => '');
throw new CloudflareRestError(
`Cloudflare REST error (${response.status}) for ${path}: ${body.slice(0, 200)}`,
response.status,
);
}
const payload = (await response.json()) as CloudflareListResponse<T>;
if (payload.result) {
all.push(...payload.result);
}
const info = payload.result_info;
if (!info || page >= (info.total_pages ?? 1)) {
break;
}
page++;
}
return all;
}
async listD1Databases(accountId: string): Promise<D1Database[]> { async listD1Databases(accountId: string): Promise<D1Database[]> {
return this.listPaginated<D1Database>(`/accounts/${encodeURIComponent(accountId)}/d1/database`); return this.listPaginated<D1Database>(`/accounts/${encodeURIComponent(accountId)}/d1/database`);
} }
@@ -76,6 +107,7 @@ export class CloudflareRestClient implements ICloudflareRestClient {
return null; return null;
} }
if (!response.ok) { if (!response.ok) {
// eslint-disable-next-line unicorn/prefer-await
const body = await response.text().catch(() => ''); const body = await response.text().catch(() => '');
throw new CloudflareRestError( throw new CloudflareRestError(
`Cloudflare REST error (${response.status}) for /zones/${zoneId}: ${body.slice(0, 200)}`, `Cloudflare REST error (${response.status}) for /zones/${zoneId}: ${body.slice(0, 200)}`,
@@ -85,34 +117,4 @@ export class CloudflareRestClient implements ICloudflareRestClient {
const payload = (await response.json()) as { result: Zone | null }; const payload = (await response.json()) as { result: Zone | null };
return payload.result ?? null; return payload.result ?? null;
} }
private async listPaginated<T>(path: string): Promise<T[]> {
const all: T[] = [];
let page = 1;
const separator = path.includes('?') ? '&' : '?';
const doFetch = this.fetchImpl ?? globalThis.fetch;
for (;;) {
const url = `${this.baseUrl}${path}${separator}page=${page}&per_page=${DEFAULT_PAGE_SIZE}`;
const response = await doFetch(url, {
headers: { Authorization: `Bearer ${this.apiToken}` },
});
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new CloudflareRestError(
`Cloudflare REST error (${response.status}) for ${path}: ${body.slice(0, 200)}`,
response.status,
);
}
const payload = (await response.json()) as CloudflareListResponse<T>;
if (payload.result) {
all.push(...payload.result);
}
const info = payload.result_info;
if (!info || page >= (info.total_pages ?? 1)) {
break;
}
page++;
}
return all;
}
} }
@@ -409,7 +409,7 @@ describe('CloudflareMetricsCollector', () => {
expect(pages?.tags.get('zone_name')).toBe('pages.example.com'); expect(pages?.tags.get('zone_name')).toBe('pages.example.com');
// Only the uncached zone should have been looked up individually. // Only the uncached zone should have been looked up individually.
expect(restClient.getZoneCalls).toEqual(['zone-pages']); expect(restClient.zoneCalls).toEqual(['zone-pages']);
const lookup = provider.metrics.find( const lookup = provider.metrics.find(
(m) => m.name === 'cloudflare_metrics_resource_lookup' && m.tags.get('resource') === 'zones_individual', (m) => m.name === 'cloudflare_metrics_resource_lookup' && m.tags.get('resource') === 'zones_individual',
+1 -1
View File
@@ -268,12 +268,12 @@ export class CloudflareMetricsCollector {
let zoneErrors = 0; let zoneErrors = 0;
for (let i = 0; i < zoneTags.length; i++) { for (let i = 0; i < zoneTags.length; i++) {
const zoneTag = zoneTags[i]; const zoneTag = zoneTags[i];
const zoneName = zoneNames[i];
if (batchResult.errors[zoneTag]) { if (batchResult.errors[zoneTag]) {
zoneErrors++; zoneErrors++;
console.error(`[collector] ${dataset.key} zone ${zoneTag} error:`, batchResult.errors[zoneTag]); console.error(`[collector] ${dataset.key} zone ${zoneTag} error:`, batchResult.errors[zoneTag]);
continue; continue;
} }
const zoneName = zoneNames[i];
const rows = batchResult.rows[zoneTag] ?? []; const rows = batchResult.rows[zoneTag] ?? [];
totalRows += rows.length; totalRows += rows.length;
for (const row of rows) { for (const row of rows) {
+1 -1
View File
@@ -6,7 +6,7 @@ describe('dataset registry invariants', () => {
for (const dataset of ALL_DATASETS) { for (const dataset of ALL_DATASETS) {
const fieldCount = Object.keys(dataset.fields).length; const fieldCount = Object.keys(dataset.fields).length;
expect(fieldCount, `${dataset.key} must have fields`).toBeGreaterThan(0); expect(fieldCount, `${dataset.key} must have fields`).toBeGreaterThan(0);
for (const [, spec] of Object.entries(dataset.fields)) { for (const spec of Object.values(dataset.fields)) {
expect(['int', 'float']).toContain(spec.type); expect(['int', 'float']).toContain(spec.type);
expect(spec.source[0]).toMatch(/^(sum|avg|max|min|quantiles|uniq|_top)$/); expect(spec.source[0]).toMatch(/^(sum|avg|max|min|quantiles|uniq|_top)$/);
} }
@@ -1,3 +1,4 @@
/* eslint-disable unicorn/no-top-level-assignment-in-function */
export const pendingFlushBuffers: string[] = []; export const pendingFlushBuffers: string[] = [];
// Cap retry buffer at 10 MB; eviction drops oldest-first. // Cap retry buffer at 10 MB; eviction drops oldest-first.
export const MAX_PENDING_FLUSH_BYTES = 10 * 1024 * 1024; export const MAX_PENDING_FLUSH_BYTES = 10 * 1024 * 1024;
@@ -43,10 +43,10 @@ const SCHEDULED_INVOCATIONS_SELECTION = `workers_scheduled: workersInvocationsSc
export function buildBatchedAccountQuery( export function buildBatchedAccountQuery(
datasets: readonly DatasetQuery[], datasets: readonly DatasetQuery[],
includeScheduledInvocations = false, shouldIncludeScheduledInvocations = false,
): string { ): string {
const selections = datasets.map((d) => buildDatasetSelection(d, d.key)); const selections = datasets.map((d) => buildDatasetSelection(d, d.key));
if (includeScheduledInvocations) { if (shouldIncludeScheduledInvocations) {
selections.push(SCHEDULED_INVOCATIONS_SELECTION); selections.push(SCHEDULED_INVOCATIONS_SELECTION);
} }
return `query CloudflareMetricsAccountBatch($accountTag: String!, $filter: JSON!) { return `query CloudflareMetricsAccountBatch($accountTag: String!, $filter: JSON!) {
@@ -136,7 +136,7 @@ describe('groupErrorsByAlias', () => {
it('returns an empty map when errors is null or undefined', () => { it('returns an empty map when errors is null or undefined', () => {
expect(groupErrorsByAlias(null)).toEqual({}); expect(groupErrorsByAlias(null)).toEqual({});
// eslint-disable-next-line unicorn/no-useless-undefined
expect(groupErrorsByAlias(undefined)).toEqual({}); expect(groupErrorsByAlias(undefined)).toEqual({});
}); });
}); });
@@ -249,6 +249,7 @@ describe('CloudflareGraphQLClient', () => {
const fetchMock = vi.fn((_url: string, init: RequestInit) => { const fetchMock = vi.fn((_url: string, init: RequestInit) => {
const body = JSON.parse(init.body as string); const body = JSON.parse(init.body as string);
// Return the aliased fields that this chunk actually asked for. // Return the aliased fields that this chunk actually asked for.
// eslint-disable-next-line unicorn/prefer-iterator-to-array
const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]); const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]);
const account: Record<string, unknown> = {}; const account: Record<string, unknown> = {};
for (const alias of aliases) { for (const alias of aliases) {
@@ -283,6 +284,7 @@ describe('CloudflareGraphQLClient', () => {
})); }));
const fetchMock = vi.fn((_url: string, init: RequestInit) => { const fetchMock = vi.fn((_url: string, init: RequestInit) => {
const body = JSON.parse(init.body as string); const body = JSON.parse(init.body as string);
// eslint-disable-next-line unicorn/prefer-iterator-to-array
const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]); const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]);
const account: Record<string, unknown> = {}; const account: Record<string, unknown> = {};
for (const alias of aliases) { for (const alias of aliases) {
@@ -393,6 +395,7 @@ describe('CloudflareGraphQLClient', () => {
const fetchMock = vi.fn((_url: string, init: RequestInit) => { const fetchMock = vi.fn((_url: string, init: RequestInit) => {
calls++; calls++;
const body = JSON.parse(init.body as string); const body = JSON.parse(init.body as string);
// eslint-disable-next-line unicorn/prefer-iterator-to-array
const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]); const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]);
const account: Record<string, unknown> = {}; const account: Record<string, unknown> = {};
for (const alias of aliases) { for (const alias of aliases) {
@@ -96,11 +96,11 @@ export class CloudflareGraphQLClient implements ICloudflareGraphQLClient {
const result: BatchedDatasetResult = { rows: {}, errors: {} }; const result: BatchedDatasetResult = { rows: {}, errors: {} };
for (const [i, chunk] of chunks.entries()) { for (const [i, chunk] of chunks.entries()) {
const includeScheduled = i === 0 && (options.includeScheduledInvocations ?? false); const isIncludeScheduled = i === 0 && (options.includeScheduledInvocations ?? false);
const chunkResult = await this.fetchAccountBatchChunk(accountTag, chunk, range, includeScheduled); const chunkResult = await this.fetchAccountBatchChunk(accountTag, chunk, range, isIncludeScheduled);
Object.assign(result.rows, chunkResult.rows); Object.assign(result.rows, chunkResult.rows);
Object.assign(result.errors, chunkResult.errors); Object.assign(result.errors, chunkResult.errors);
if (includeScheduled) { if (isIncludeScheduled) {
if (chunkResult.errors.workers_scheduled) { if (chunkResult.errors.workers_scheduled) {
result.errors.workers_scheduled = chunkResult.errors.workers_scheduled; result.errors.workers_scheduled = chunkResult.errors.workers_scheduled;
} else { } else {
@@ -116,10 +116,10 @@ export class CloudflareGraphQLClient implements ICloudflareGraphQLClient {
accountTag: string, accountTag: string,
datasets: readonly DatasetQuery[], datasets: readonly DatasetQuery[],
range: { start: Date; end: Date }, range: { start: Date; end: Date },
includeScheduledInvocations: boolean, shouldIncludeScheduledInvocations: boolean,
attempt = 1, attempt = 1,
): Promise<BatchedDatasetResult> { ): Promise<BatchedDatasetResult> {
const query = buildBatchedAccountQuery(datasets, includeScheduledInvocations); const query = buildBatchedAccountQuery(datasets, shouldIncludeScheduledInvocations);
const variables = { const variables = {
accountTag, accountTag,
filter: buildFilterObject(datasets[0] ?? null, range), filter: buildFilterObject(datasets[0] ?? null, range),
@@ -138,7 +138,7 @@ export class CloudflareGraphQLClient implements ICloudflareGraphQLClient {
result.rows[dataset.key] = rows ?? []; result.rows[dataset.key] = rows ?? [];
} }
if (includeScheduledInvocations) { if (shouldIncludeScheduledInvocations) {
if (errorsByAlias.workers_scheduled) { if (errorsByAlias.workers_scheduled) {
result.errors.workers_scheduled = errorsByAlias.workers_scheduled; result.errors.workers_scheduled = errorsByAlias.workers_scheduled;
} else { } else {
@@ -153,14 +153,14 @@ export class CloudflareGraphQLClient implements ICloudflareGraphQLClient {
for (const dataset of datasets) { for (const dataset of datasets) {
result.errors[dataset.key] = message; result.errors[dataset.key] = message;
} }
if (includeScheduledInvocations) { if (shouldIncludeScheduledInvocations) {
result.errors.workers_scheduled = message; result.errors.workers_scheduled = message;
} }
} }
// Retry once when all fields errored — likely a transient Cloudflare // Retry once when all fields errored — likely a transient Cloudflare
// analytics backend issue that resolves on the next attempt. // analytics backend issue that resolves on the next attempt.
const totalFields = datasets.length + (includeScheduledInvocations ? 1 : 0); const totalFields = datasets.length + (shouldIncludeScheduledInvocations ? 1 : 0);
if (attempt < 2 && totalFields > 0 && Object.keys(result.errors).length >= totalFields) { if (attempt < 2 && totalFields > 0 && Object.keys(result.errors).length >= totalFields) {
this._retryCount++; this._retryCount++;
console.warn(`[graphql] all ${totalFields} fields errored, retrying chunk (attempt ${attempt + 1})`); console.warn(`[graphql] all ${totalFields} fields errored, retrying chunk (attempt ${attempt + 1})`);
@@ -172,7 +172,7 @@ export class CloudflareGraphQLClient implements ICloudflareGraphQLClient {
accountTag, accountTag,
datasets, datasets,
range, range,
includeScheduledInvocations, shouldIncludeScheduledInvocations,
attempt + 1, attempt + 1,
); );
if (Object.keys(retryResult.errors).length < totalFields) { if (Object.keys(retryResult.errors).length < totalFields) {
+7 -9
View File
@@ -1,6 +1,6 @@
import { createExecutionContext, createScheduledController, waitOnExecutionContext } from 'cloudflare:test'; import { createExecutionContext, createScheduledController, waitOnExecutionContext } from 'cloudflare:test';
import { env, exports } from 'cloudflare:workers'; import { env, exports } from 'cloudflare:workers';
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { __resetFlushStateForTests } from './flush-state.js'; import { __resetFlushStateForTests } from './flush-state.js';
import worker from './index.js'; import worker from './index.js';
@@ -31,16 +31,15 @@ describe('scheduled handler', () => {
it('emits cron_error{reason=missing_config} when API token or account ID are missing', async () => { it('emits cron_error{reason=missing_config} when API token or account ID are missing', async () => {
// Collect metric flush bodies seen by the victoria-metrics POST so we // Collect metric flush bodies seen by the victoria-metrics POST so we
// can assert on the cron_error line without touching internal state. // can assert on the cron_error line without touching internal state.
const originalFetch = globalThis.fetch;
const flushBodies: string[] = []; const flushBodies: string[] = [];
globalThis.fetch = ((url: RequestInfo | URL, init?: RequestInit) => { vi.stubGlobal('fetch', (url: RequestInfo | URL, init?: RequestInit) => {
const target = typeof url === 'string' ? url : url.toString(); const target = typeof url === 'string' ? url : url.toString();
if (target.includes('/write')) { if (target.includes('/write')) {
flushBodies.push(init?.body as string); flushBodies.push(init?.body as string);
return Promise.resolve(new Response('', { status: 204 })); return Promise.resolve(new Response('', { status: 204 }));
} }
return Promise.resolve(new Response('', { status: 200 })); return Promise.resolve(new Response('', { status: 200 }));
}) as typeof fetch; });
const controller = createScheduledController(); const controller = createScheduledController();
const ctx = createExecutionContext(); const ctx = createExecutionContext();
@@ -51,7 +50,7 @@ describe('scheduled handler', () => {
await worker.scheduled?.(controller, env as unknown as Env, ctx); await worker.scheduled?.(controller, env as unknown as Env, ctx);
await waitOnExecutionContext(ctx); await waitOnExecutionContext(ctx);
} finally { } finally {
globalThis.fetch = originalFetch; vi.unstubAllGlobals();
} }
const combined = flushBodies.join('\n'); const combined = flushBodies.join('\n');
@@ -63,16 +62,15 @@ describe('scheduled handler', () => {
// First tick: a successful flush populates lastFlushStats. // First tick: a successful flush populates lastFlushStats.
// Second tick: takeLastFlushStats() drains it and emits // Second tick: takeLastFlushStats() drains it and emits
// cloudflare_metrics_flush_* lines to the second tick's flush body. // cloudflare_metrics_flush_* lines to the second tick's flush body.
const originalFetch = globalThis.fetch;
const flushBodies: string[] = []; const flushBodies: string[] = [];
globalThis.fetch = ((url: RequestInfo | URL, init?: RequestInit) => { vi.stubGlobal('fetch', (url: RequestInfo | URL, init?: RequestInit) => {
const target = typeof url === 'string' ? url : url.toString(); const target = typeof url === 'string' ? url : url.toString();
if (target.includes('/write')) { if (target.includes('/write')) {
flushBodies.push(init?.body as string); flushBodies.push(init?.body as string);
return Promise.resolve(new Response('', { status: 204 })); return Promise.resolve(new Response('', { status: 204 }));
} }
return Promise.resolve(new Response('', { status: 200 })); return Promise.resolve(new Response('', { status: 200 }));
}) as typeof fetch; });
try { try {
const controller = createScheduledController(); const controller = createScheduledController();
@@ -86,7 +84,7 @@ describe('scheduled handler', () => {
await worker.scheduled?.(controller, env as unknown as Env, ctx2); await worker.scheduled?.(controller, env as unknown as Env, ctx2);
await waitOnExecutionContext(ctx2); await waitOnExecutionContext(ctx2);
} finally { } finally {
globalThis.fetch = originalFetch; vi.unstubAllGlobals();
} }
expect(flushBodies.length).toBeGreaterThanOrEqual(2); expect(flushBodies.length).toBeGreaterThanOrEqual(2);
@@ -1,3 +1,4 @@
/* eslint-disable unicorn/no-top-level-assignment-in-function */
import { CloudflareRestClient } from '../cloudflare-api.js'; import { CloudflareRestClient } from '../cloudflare-api.js';
import { CloudflareMetricsCollector } from '../collector.js'; import { CloudflareMetricsCollector } from '../collector.js';
import { ALL_DATASETS } from '../datasets.js'; import { ALL_DATASETS } from '../datasets.js';
@@ -88,7 +88,7 @@ describe.skipIf(!hasCredentials)('Cloudflare GraphQL integration', () => {
expect(row.dimensions).toBeDefined(); expect(row.dimensions).toBeDefined();
const timestampDim = dataset.timestampDimension ?? 'datetimeMinute'; const timestampDim = dataset.timestampDimension ?? 'datetimeMinute';
expect(row.dimensions[timestampDim]).toBeDefined(); expect(row.dimensions[timestampDim]).toBeDefined();
for (const [, spec] of Object.entries(dataset.fields)) { for (const spec of Object.values(dataset.fields)) {
const [block, key] = spec.source; const [block, key] = spec.source;
if (block === '_top') { if (block === '_top') {
continue; continue;
@@ -68,7 +68,7 @@ describe('InfluxDB line protocol NaN/Infinity handling', () => {
it('skips NaN float fields', () => { it('skips NaN float fields', () => {
const provider = new InfluxMetricsProvider('', ''); const provider = new InfluxMetricsProvider('', '');
const metric = Metric.create('test').floatField('bad', Number.NaN).intField('good', 1); const metric = Metric.create('test').floatField('bad', NaN).intField('good', 1);
provider.pushMetric(metric); provider.pushMetric(metric);
const [line] = getLines(provider); const [line] = getLines(provider);
expect(line).not.toContain('bad'); expect(line).not.toContain('bad');
@@ -95,7 +95,7 @@ describe('InfluxDB line protocol NaN/Infinity handling', () => {
it('drops the entire line when all fields are NaN/Infinity', () => { it('drops the entire line when all fields are NaN/Infinity', () => {
const provider = new InfluxMetricsProvider('', ''); const provider = new InfluxMetricsProvider('', '');
const metric = Metric.create('test').floatField('a', Number.NaN).floatField('b', Infinity); const metric = Metric.create('test').floatField('a', NaN).floatField('b', Infinity);
provider.pushMetric(metric); provider.pushMetric(metric);
expect(getLines(provider)).toHaveLength(0); expect(getLines(provider)).toHaveLength(0);
}); });
@@ -16,10 +16,12 @@ export class HeaderMetricsProvider implements IMetricsProviderRepository {
pushMetric(metric: Metric) { pushMetric(metric: Metric) {
for (const [label, { value, type }] of metric.fields) { for (const [label, { value, type }] of metric.fields) {
if (type === 'duration') { if (type !== 'duration') {
const suffix = label === 'duration' ? '' : `_${label.replace('_duration', '')}`; continue;
this._metrics.push(`${metric.name}${suffix};dur=${value}`);
} }
const suffix = label === 'duration' ? '' : `_${label.replace('_duration', '')}`;
this._metrics.push(`${metric.name}${suffix};dur=${value}`);
} }
} }
+12 -19
View File
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { __resetFlushStateForTests, takeLastFlushStats } from './flush-state.js'; import { __resetFlushStateForTests, takeLastFlushStats } from './flush-state.js';
import { InfluxMetricsProvider, type IMetricsProviderRepository } from './metric-providers.js'; import { InfluxMetricsProvider, type IMetricsProviderRepository } from './metric-providers.js';
import { Metric } from './metric.js'; import { Metric } from './metric.js';
@@ -74,8 +74,7 @@ describe('InfluxMetricsProvider flush self-telemetry', () => {
}); });
it('records stats with status=ok after a successful flush', async () => { it('records stats with status=ok after a successful flush', async () => {
const originalFetch = globalThis.fetch; vi.stubGlobal('fetch', () => Promise.resolve(new Response('', { status: 204 })));
globalThis.fetch = (() => Promise.resolve(new Response('', { status: 204 }))) as typeof fetch;
try { try {
const provider = new InfluxMetricsProvider('token', 'prod'); const provider = new InfluxMetricsProvider('token', 'prod');
provider.pushMetric(Metric.create('foo').intField('v', 1)); provider.pushMetric(Metric.create('foo').intField('v', 1));
@@ -86,13 +85,12 @@ describe('InfluxMetricsProvider flush self-telemetry', () => {
expect(stats?.pendingBuffers).toBe(0); expect(stats?.pendingBuffers).toBe(0);
expect(stats?.pendingBytes).toBe(0); expect(stats?.pendingBytes).toBe(0);
} finally { } finally {
globalThis.fetch = originalFetch; vi.unstubAllGlobals();
} }
}); });
it('records stats with status=error after a failed flush', async () => { it('records stats with status=error after a failed flush', async () => {
const originalFetch = globalThis.fetch; vi.stubGlobal('fetch', () => Promise.resolve(new Response('bad', { status: 502 })));
globalThis.fetch = (() => Promise.resolve(new Response('bad', { status: 502 }))) as typeof fetch;
try { try {
const provider = new InfluxMetricsProvider('token', 'prod'); const provider = new InfluxMetricsProvider('token', 'prod');
provider.pushMetric(Metric.create('foo').intField('v', 1)); provider.pushMetric(Metric.create('foo').intField('v', 1));
@@ -104,13 +102,12 @@ describe('InfluxMetricsProvider flush self-telemetry', () => {
expect(stats?.pendingBuffers).toBe(1); expect(stats?.pendingBuffers).toBe(1);
expect(stats?.pendingBytes).toBeGreaterThan(0); expect(stats?.pendingBytes).toBeGreaterThan(0);
} finally { } finally {
globalThis.fetch = originalFetch; vi.unstubAllGlobals();
} }
}); });
it('clears lastFlushStats after a single take', async () => { it('clears lastFlushStats after a single take', async () => {
const originalFetch = globalThis.fetch; vi.stubGlobal('fetch', () => Promise.resolve(new Response('', { status: 204 })));
globalThis.fetch = (() => Promise.resolve(new Response('', { status: 204 }))) as typeof fetch;
try { try {
const provider = new InfluxMetricsProvider('token', 'prod'); const provider = new InfluxMetricsProvider('token', 'prod');
provider.pushMetric(Metric.create('foo').intField('v', 1)); provider.pushMetric(Metric.create('foo').intField('v', 1));
@@ -118,14 +115,13 @@ describe('InfluxMetricsProvider flush self-telemetry', () => {
expect(takeLastFlushStats()).not.toBeNull(); expect(takeLastFlushStats()).not.toBeNull();
expect(takeLastFlushStats()).toBeNull(); expect(takeLastFlushStats()).toBeNull();
} finally { } finally {
globalThis.fetch = originalFetch; vi.unstubAllGlobals();
} }
}); });
it('evicts the oldest stashed flush body when the total exceeds the 10 MB cap', async () => { it('evicts the oldest stashed flush body when the total exceeds the 10 MB cap', async () => {
const originalFetch = globalThis.fetch;
// Always fail so every flush stashes its body in the retry buffer. // Always fail so every flush stashes its body in the retry buffer.
globalThis.fetch = (() => Promise.resolve(new Response('', { status: 502 }))) as typeof fetch; vi.stubGlobal('fetch', () => Promise.resolve(new Response('', { status: 502 })));
try { try {
// Each payload is ~4 MB of raw line protocol; three of them land us // Each payload is ~4 MB of raw line protocol; three of them land us
// over the 10 MB cap and force an eviction. // over the 10 MB cap and force an eviction.
@@ -146,7 +142,7 @@ describe('InfluxMetricsProvider flush self-telemetry', () => {
expect(stats?.pendingBuffers).toBe(2); expect(stats?.pendingBuffers).toBe(2);
expect(stats?.pendingBytes).toBeLessThan(10 * 1024 * 1024 + 4 * 1024 * 1024); expect(stats?.pendingBytes).toBeLessThan(10 * 1024 * 1024 + 4 * 1024 * 1024);
} finally { } finally {
globalThis.fetch = originalFetch; vi.unstubAllGlobals();
} }
}); });
}); });
@@ -185,12 +181,9 @@ describe('InfluxMetricsProvider line protocol', () => {
}); });
it('stashes a failed flush body and resends it on the next successful flush', async () => { it('stashes a failed flush body and resends it on the next successful flush', async () => {
// Use a fetch mock via globalThis so the provider picks it up through its
// real `fetch` call path.
const originalFetch = globalThis.fetch;
let call = 0; let call = 0;
const received: string[] = []; const received: string[] = [];
globalThis.fetch = ((_url: RequestInfo | URL, init?: RequestInit) => { vi.stubGlobal('fetch', (_url: RequestInfo | URL, init?: RequestInit) => {
call++; call++;
received.push(init?.body as string); received.push(init?.body as string);
if (call === 1) { if (call === 1) {
@@ -198,7 +191,7 @@ describe('InfluxMetricsProvider line protocol', () => {
return Promise.resolve(new Response('bad gateway', { status: 502 })); return Promise.resolve(new Response('bad gateway', { status: 502 }));
} }
return Promise.resolve(new Response('', { status: 204 })); return Promise.resolve(new Response('', { status: 204 }));
}) as typeof fetch; });
try { try {
const provider = new InfluxMetricsProvider('token', 'prod'); const provider = new InfluxMetricsProvider('token', 'prod');
@@ -216,7 +209,7 @@ describe('InfluxMetricsProvider line protocol', () => {
expect(received[1]).toContain('first'); expect(received[1]).toContain('first');
expect(received[1]).toContain('second'); expect(received[1]).toContain('second');
} finally { } finally {
globalThis.fetch = originalFetch; vi.unstubAllGlobals();
} }
}); });
}); });
+1 -1
View File
@@ -26,7 +26,7 @@ export function monitorAsyncFunction<T extends AsyncFn>(
try { try {
return await call(...args); return await call(...args);
} catch (error) { } catch (error) {
if (!acceptedErrors.some((acceptedError) => error instanceof acceptedError)) { if (acceptedErrors.every((acceptedError) => !(error instanceof acceptedError))) {
console.error(error, `${operationName}_errors`); console.error(error, `${operationName}_errors`);
metric.intField('errors', 1); metric.intField('errors', 1);
} }
@@ -113,7 +113,7 @@ describe('ResourceCacheService', () => {
const service = new ResourceCacheService('acct', metrics, now, restClient); const service = new ResourceCacheService('acct', metrics, now, restClient);
await service.populate(); await service.populate();
await service.resolveMissingZones(['zone-1']); await service.resolveMissingZones(['zone-1']);
expect(restClient.getZoneCalls).toHaveLength(0); expect(restClient.zoneCalls).toHaveLength(0);
}); });
it('caps individual lookups per run', async () => { it('caps individual lookups per run', async () => {
@@ -125,7 +125,7 @@ describe('ResourceCacheService', () => {
const service = new ResourceCacheService('acct', metrics, now, restClient); const service = new ResourceCacheService('acct', metrics, now, restClient);
await service.populate(); await service.populate();
await service.resolveMissingZones(Object.keys(zones)); await service.resolveMissingZones(Object.keys(zones));
expect(restClient.getZoneCalls).toHaveLength(20); expect(restClient.zoneCalls).toHaveLength(20);
}); });
it('does nothing when no rest client is provided', async () => { it('does nothing when no rest client is provided', async () => {
+26 -16
View File
@@ -1,3 +1,4 @@
/* eslint-disable unicorn/no-top-level-assignment-in-function */
import type { ICloudflareRestClient } from './cloudflare-api.js'; import type { ICloudflareRestClient } from './cloudflare-api.js';
import { Metric } from './metric.js'; import { Metric } from './metric.js';
import type { CloudflareMetricsRepository } from './metrics.js'; import type { CloudflareMetricsRepository } from './metrics.js';
@@ -75,25 +76,28 @@ export class ResourceCacheService {
const restClient = this.restClient; const restClient = this.restClient;
const now = this.now().getTime(); const now = this.now().getTime();
const cacheFresh = <T>(cache: CachedResourceLookup<T> | null): cache is CachedResourceLookup<T> => const isCacheFresh = <T>(cache: CachedResourceLookup<T> | null): cache is CachedResourceLookup<T> =>
cache !== null && now - cache.loadedAt < RESOURCE_CACHE_TTL_MS; cache !== null && now - cache.loadedAt < RESOURCE_CACHE_TTL_MS;
const [d1Result, queuesResult, zonesResult] = await Promise.allSettled([ const [d1Result, queuesResult, zonesResult] = await Promise.allSettled([
cacheFresh(cachedD1Databases) isCacheFresh(cachedD1Databases)
? Promise.resolve(cachedD1Databases.values) ? Promise.resolve(cachedD1Databases.values)
: restClient.listD1Databases(this.accountTag).then((values) => { : // eslint-disable-next-line unicorn/prefer-await
restClient.listD1Databases(this.accountTag).then((values) => {
cachedD1Databases = { values, loadedAt: now }; cachedD1Databases = { values, loadedAt: now };
return values; return values;
}), }),
cacheFresh(cachedQueues) isCacheFresh(cachedQueues)
? Promise.resolve(cachedQueues.values) ? Promise.resolve(cachedQueues.values)
: restClient.listQueues(this.accountTag).then((values) => { : // eslint-disable-next-line unicorn/prefer-await
restClient.listQueues(this.accountTag).then((values) => {
cachedQueues = { values, loadedAt: now }; cachedQueues = { values, loadedAt: now };
return values; return values;
}), }),
cacheFresh(cachedBulkZones) isCacheFresh(cachedBulkZones)
? Promise.resolve(cachedBulkZones.values) ? Promise.resolve(cachedBulkZones.values)
: restClient.listZones(this.accountTag).then((values) => { : // eslint-disable-next-line unicorn/prefer-await
restClient.listZones(this.accountTag).then((values) => {
cachedBulkZones = { values, loadedAt: now }; cachedBulkZones = { values, loadedAt: now };
return values; return values;
}), }),
@@ -101,26 +105,32 @@ export class ResourceCacheService {
this.recordResourceLookup('d1_databases', d1Result, (items) => { this.recordResourceLookup('d1_databases', d1Result, (items) => {
for (const db of items) { for (const db of items) {
if (db.uuid && db.name) { if (!(db.uuid && db.name)) {
this.cache.d1Databases.set(db.uuid, db.name); continue;
globalD1NameCache.set(db.uuid, db.name);
} }
this.cache.d1Databases.set(db.uuid, db.name);
globalD1NameCache.set(db.uuid, db.name);
} }
}); });
this.recordResourceLookup('queues', queuesResult, (items) => { this.recordResourceLookup('queues', queuesResult, (items) => {
for (const q of items) { for (const q of items) {
if (q.queue_id && q.queue_name) { if (!(q.queue_id && q.queue_name)) {
this.cache.queues.set(q.queue_id, q.queue_name); continue;
globalQueueNameCache.set(q.queue_id, q.queue_name);
} }
this.cache.queues.set(q.queue_id, q.queue_name);
globalQueueNameCache.set(q.queue_id, q.queue_name);
} }
}); });
this.recordResourceLookup('zones', zonesResult, (items) => { this.recordResourceLookup('zones', zonesResult, (items) => {
for (const z of items) { for (const z of items) {
if (z.id && z.name) { if (!(z.id && z.name)) {
this.cache.zones.set(z.id, z.name); continue;
this.cache.bulkZoneTags.add(z.id);
} }
this.cache.zones.set(z.id, z.name);
this.cache.bulkZoneTags.add(z.id);
} }
}); });
+2 -2
View File
@@ -109,7 +109,7 @@ export function asGraphQLClient(fake: FakeGraphQLClient): CloudflareGraphQLClien
* pages-only zone exists outside the bulk list. * pages-only zone exists outside the bulk list.
*/ */
export class FakeRestClient implements ICloudflareRestClient { export class FakeRestClient implements ICloudflareRestClient {
public getZoneCalls: string[] = []; public zoneCalls: string[] = [];
constructor( constructor(
public readonly d1: Array<{ uuid: string; name: string }> = [], public readonly d1: Array<{ uuid: string; name: string }> = [],
public readonly queues: Array<{ queue_id: string; queue_name: string }> = [], public readonly queues: Array<{ queue_id: string; queue_name: string }> = [],
@@ -134,7 +134,7 @@ export class FakeRestClient implements ICloudflareRestClient {
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
async getZone(zoneId: string) { async getZone(zoneId: string) {
this.getZoneCalls.push(zoneId); this.zoneCalls.push(zoneId);
if (zoneId in this.individualZones) { if (zoneId in this.individualZones) {
return this.individualZones[zoneId]; return this.individualZones[zoneId];
} }
+1 -1
View File
@@ -120,7 +120,7 @@ export class ApprovalValidator {
console.log( console.log(
`[approval] DEBUG: Reviews by valid users (after deduplication):`, `[approval] DEBUG: Reviews by valid users (after deduplication):`,
JSON.stringify( JSON.stringify(
[...approvalsByUser.entries()].map(([userId, review]) => ({ [...approvalsByUser].map(([userId, review]) => ({
userId, userId,
login: review.user.login, login: review.user.login,
state: review.state, state: review.state,
+1 -1
View File
@@ -74,7 +74,7 @@ export function createOctokitForInstallation(appId: string, privateKey: string,
console.error('Invalid privateKey:', { console.error('Invalid privateKey:', {
hasPrivateKey: !!privateKey, hasPrivateKey: !!privateKey,
type: typeof privateKey, type: typeof privateKey,
length: privateKey ? String(privateKey).length : 0, length: privateKey ? privateKey.length : 0,
}); });
throw new Error('Invalid GitHub App Private Key provided to createOctokitForInstallation'); throw new Error('Invalid GitHub App Private Key provided to createOctokitForInstallation');
} }
+2 -2
View File
@@ -22,11 +22,11 @@ export function getDevModeConfig(env: Env): DevModeConfig {
// Extract PR number from stage (e.g., '-pr-123' -> 123) // Extract PR number from stage (e.g., '-pr-123' -> 123)
let prNumber: number | undefined; let prNumber: number | undefined;
if (env.DEV_PR_NUMBER) { if (env.DEV_PR_NUMBER) {
prNumber = Number.parseInt(env.DEV_PR_NUMBER, 10); prNumber = Number(env.DEV_PR_NUMBER);
} else if (isPRDeployment && env.STAGE) { } else if (isPRDeployment && env.STAGE) {
const match = env.STAGE.match(/-pr-(\d+)/); const match = env.STAGE.match(/-pr-(\d+)/);
if (match) { if (match) {
prNumber = Number.parseInt(match[1], 10); prNumber = Number(match[1]);
} }
} }
+2 -2
View File
@@ -30,13 +30,13 @@ export async function verifyWebhookSignature(body: string, signature: string, se
const computedSignature = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join(''); const computedSignature = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');
// Constant-time comparison to prevent timing attacks // Constant-time comparison to prevent timing attacks
return safeCompare(computedSignature, providedSignature); return isSafeCompare(computedSignature, providedSignature);
} }
/** /**
* Constant-time string comparison to prevent timing attacks * Constant-time string comparison to prevent timing attacks
*/ */
function safeCompare(a: string, b: string): boolean { function isSafeCompare(a: string, b: string): boolean {
if (a.length !== b.length) { if (a.length !== b.length) {
return false; return false;
} }
+1 -1
View File
@@ -774,7 +774,7 @@ describe('GitHubRepository', () => {
if (url.origin === 'https://api.github.com' && url.pathname === '/repos/immich-app/immich/releases') { if (url.origin === 'https://api.github.com' && url.pathname === '/repos/immich-app/immich/releases') {
return Promise.resolve(Response.json(githubReleases)); return Promise.resolve(Response.json(githubReleases));
} }
return Promise.reject(new Error(`unexpected fetch: ${url.toString()}`)); return Promise.reject(new Error(`unexpected fetch: ${url.href}`));
}); });
const releases = await new GitHubRepository().fetchReleases(); const releases = await new GitHubRepository().fetchReleases();
+2 -2
View File
@@ -111,7 +111,7 @@ export default {
if (env.ENVIRONMENT) { if (env.ENVIRONMENT) {
const cache = caches.default; const cache = caches.default;
const cacheKey = new Request(url.toString(), request); const cacheKey = new Request(url.href, request);
const cached = await cache.match(cacheKey); const cached = await cache.match(cacheKey);
if (cached) { if (cached) {
@@ -132,7 +132,7 @@ export default {
const response = jsonResponse(changelog, 200, { 'Cache-Control': 'public, max-age=86400' }); const response = jsonResponse(changelog, 200, { 'Cache-Control': 'public, max-age=86400' });
if (env.ENVIRONMENT) { if (env.ENVIRONMENT) {
const cache = caches.default; const cache = caches.default;
const cacheKey = new Request(url.toString(), request); const cacheKey = new Request(url.href, request);
ctx.waitUntil(cache.put(cacheKey, response.clone())); ctx.waitUntil(cache.put(cacheKey, response.clone()));
} }
return response; return response;
+5 -3
View File
@@ -75,10 +75,12 @@ export class HeaderMetricsProvider implements IMetricsProviderRepository {
pushMetric(metric: Metric) { pushMetric(metric: Metric) {
for (const [label, { value, type }] of metric.fields) { for (const [label, { value, type }] of metric.fields) {
if (type === 'duration') { if (type !== 'duration') {
const suffix = label === 'duration' ? '' : `_${label.replace('_duration', '')}`; continue;
this._metrics.push(`${metric.name}${suffix};dur=${value}`);
} }
const suffix = label === 'duration' ? '' : `_${label.replace('_duration', '')}`;
this._metrics.push(`${metric.name}${suffix};dur=${value}`);
} }
} }
+1 -1
View File
@@ -26,7 +26,7 @@ export function monitorAsyncFunction<T extends AsyncFn>(
try { try {
return await call(...args); return await call(...args);
} catch (error) { } catch (error) {
if (!acceptedErrors.some((acceptedError) => error instanceof acceptedError)) { if (acceptedErrors.every((acceptedError) => !(error instanceof acceptedError))) {
console.error(error, `${operationName}_errors`); console.error(error, `${operationName}_errors`);
metric.intField('errors', 1); metric.intField('errors', 1);
} }
+1 -1
View File
@@ -55,7 +55,7 @@ export class VersionService {
})(); })();
const response = new Map<ReleaseChannel, VersionResponse>( const response = new Map<ReleaseChannel, VersionResponse>(
[...latest.entries()] [...latest]
.filter(([_, release]) => release !== null) .filter(([_, release]) => release !== null)
.map( .map(
([channel, release]) => ([channel, release]) =>
+2 -2
View File
@@ -16,10 +16,10 @@ export async function verifyWebhookSignature(body: string, signature: string, se
const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body)); const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body));
const computedSignature = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join(''); const computedSignature = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');
return timingSafeEqual(computedSignature, providedSignature); return isTimingSafeEqual(computedSignature, providedSignature);
} }
function timingSafeEqual(a: string, b: string): boolean { function isTimingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) { if (a.length !== b.length) {
return false; return false;
} }
+5 -1
View File
@@ -39,7 +39,7 @@ export default typescriptEslint.config([
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-floating-promises': 'error',
'unicorn/prevent-abbreviations': 'off', 'unicorn/name-replacements': 'off',
'unicorn/filename-case': 'off', 'unicorn/filename-case': 'off',
'unicorn/no-null': 'off', 'unicorn/no-null': 'off',
'unicorn/prefer-top-level-await': 'off', 'unicorn/prefer-top-level-await': 'off',
@@ -48,6 +48,10 @@ export default typescriptEslint.config([
'unicorn/import-style': 'off', 'unicorn/import-style': 'off',
'unicorn/prefer-structured-clone': 'off', 'unicorn/prefer-structured-clone': 'off',
'unicorn/no-for-loop': 'off', 'unicorn/no-for-loop': 'off',
'unicorn/consistent-class-member-order': 'off',
'unicorn/no-computed-property-existence-check': 'off',
'unicorn/max-nested-calls': ['error', { max: 4 }],
'unicorn/no-break-in-nested-loop': 'off',
'@typescript-eslint/await-thenable': 'error', '@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/no-misused-promises': 'error',
'require-await': 'off', 'require-await': 'off',
+1 -1
View File
@@ -22,7 +22,7 @@
"eslint": "^10.0.0", "eslint": "^10.0.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-unicorn": "^65.0.0", "eslint-plugin-unicorn": "^70.0.0",
"globals": "^17.0.0", "globals": "^17.0.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.2.0", "prettier-plugin-organize-imports": "^4.2.0",
+8 -7
View File
@@ -33,8 +33,8 @@ importers:
specifier: ^5.5.4 specifier: ^5.5.4
version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0))(eslint@10.6.0)(prettier@3.9.4) version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0))(eslint@10.6.0)(prettier@3.9.4)
eslint-plugin-unicorn: eslint-plugin-unicorn:
specifier: ^65.0.0 specifier: ^70.0.0
version: 65.0.1(eslint@10.6.0) version: 70.0.0(eslint@10.6.0)
globals: globals:
specifier: ^17.0.0 specifier: ^17.0.0
version: 17.7.0 version: 17.7.0
@@ -1032,11 +1032,11 @@ packages:
eslint-config-prettier: eslint-config-prettier:
optional: true optional: true
eslint-plugin-unicorn@65.0.1: eslint-plugin-unicorn@70.0.0:
resolution: {integrity: sha512-daCrQrgxOoOz2uMPWB3Y3vvv/5q+ncwICI8IjoebiwtW87CaY4tAN5EEiRXTYVnf7qi1v1BGBdHOSnZLV0rx6A==} resolution: {integrity: sha512-uAF9xMcVvvhTfvusCgogJ1wh4To3q2KhVMw3i1Apf/ILTbxsCjscvraAZACsEurb7no2fdXblD3whcbVnjw5zg==}
engines: {node: ^20.10.0 || >=21.0.0} engines: {node: '>=22'}
peerDependencies: peerDependencies:
eslint: '>=9.38.0' eslint: '>=10.4'
eslint-scope@9.1.2: eslint-scope@9.1.2:
resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
@@ -2436,10 +2436,11 @@ snapshots:
optionalDependencies: optionalDependencies:
eslint-config-prettier: 10.1.8(eslint@10.6.0) eslint-config-prettier: 10.1.8(eslint@10.6.0)
eslint-plugin-unicorn@65.0.1(eslint@10.6.0): eslint-plugin-unicorn@70.0.0(eslint@10.6.0):
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.29.7 '@babel/helper-validator-identifier': 7.29.7
'@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0)
browserslist: 4.28.4
change-case: 5.4.4 change-case: 5.4.4
ci-info: 4.4.0 ci-info: 4.4.0
core-js-compat: 3.49.0 core-js-compat: 3.49.0