refactor: deepen query failure classification module
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { QueryResult } from './query/utils.js';
|
||||
import type { QueryRegistry } from './query/registry.js';
|
||||
import type { TransportMode } from './gsd-transport-policy.js';
|
||||
import { isTimeoutLikeError } from './query-failure-classification.js';
|
||||
|
||||
export interface TransportRequest {
|
||||
legacyCommand: string;
|
||||
@@ -24,12 +25,6 @@ export interface TransportPolicyLike {
|
||||
allowFallbackToSubprocess: boolean;
|
||||
}
|
||||
|
||||
function isTimeoutLikeError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) return false;
|
||||
if (error.name === 'TimeoutError' || error.name === 'AbortError') return true;
|
||||
return error.message.includes('timed out after');
|
||||
}
|
||||
|
||||
export class GSDTransport {
|
||||
constructor(
|
||||
private readonly registry: QueryRegistry,
|
||||
|
||||
28
sdk/src/query-failure-classification.test.ts
Normal file
28
sdk/src/query-failure-classification.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
errorMessage,
|
||||
isTimeoutLikeError,
|
||||
isTimeoutMessage,
|
||||
parseTimeoutMs,
|
||||
timeoutMessage,
|
||||
} from './query-failure-classification.js';
|
||||
|
||||
describe('query failure classification', () => {
|
||||
it('extracts timeout metadata from message', () => {
|
||||
const msg = timeoutMessage('state', ['load'], 30000);
|
||||
expect(isTimeoutMessage(msg)).toBe(true);
|
||||
expect(parseTimeoutMs(msg)).toBe(30000);
|
||||
});
|
||||
|
||||
it('classifies timeout-like errors', () => {
|
||||
expect(isTimeoutLikeError(new Error('gsd-tools timed out after 1000ms: x'))).toBe(true);
|
||||
const abort = new Error('aborted');
|
||||
abort.name = 'AbortError';
|
||||
expect(isTimeoutLikeError(abort)).toBe(true);
|
||||
});
|
||||
|
||||
it('normalizes unknown error values', () => {
|
||||
expect(errorMessage('boom')).toBe('boom');
|
||||
expect(errorMessage(new Error('x'))).toBe('x');
|
||||
});
|
||||
});
|
||||
24
sdk/src/query-failure-classification.ts
Normal file
24
sdk/src/query-failure-classification.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
export function parseTimeoutMs(message: string): number | undefined {
|
||||
const m = message.match(/timed out after\s+(\d+)ms/i);
|
||||
if (!m) return undefined;
|
||||
const n = Number.parseInt(m[1], 10);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
export function isTimeoutMessage(message: string): boolean {
|
||||
return /timed out after/i.test(message);
|
||||
}
|
||||
|
||||
export function isTimeoutLikeError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) return false;
|
||||
if (error.name === 'TimeoutError' || error.name === 'AbortError') return true;
|
||||
return isTimeoutMessage(error.message);
|
||||
}
|
||||
|
||||
export function timeoutMessage(command: string, args: string[], timeoutMs: number): string {
|
||||
return `gsd-tools timed out after ${timeoutMs}ms: ${command} ${args.join(' ')}`;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatQueryRawOutput } from './query-raw-output-projection.js';
|
||||
import { timeoutMessage } from './query-failure-classification.js';
|
||||
import type { QueryResult } from './query/utils.js';
|
||||
|
||||
export interface QueryNativeDirectAdapterDeps {
|
||||
@@ -33,7 +34,7 @@ export class QueryNativeDirectAdapter {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(
|
||||
this.deps.createTimeoutError(
|
||||
`gsd-tools timed out after ${this.deps.timeoutMs}ms: ${legacyCommand} ${legacyArgs.join(' ')}`,
|
||||
timeoutMessage(legacyCommand, legacyArgs, this.deps.timeoutMs),
|
||||
legacyCommand,
|
||||
legacyArgs,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { timeoutMessage } from './query-failure-classification.js';
|
||||
import type { GSDToolsError } from './gsd-tools-error.js';
|
||||
|
||||
export interface QuerySubprocessAdapterDeps {
|
||||
@@ -40,7 +41,7 @@ export class QuerySubprocessAdapter {
|
||||
if (error.killed || (error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
||||
reject(
|
||||
this.deps.createToolsError(
|
||||
`gsd-tools timed out after ${this.deps.timeoutMs}ms: ${command} ${args.join(' ')}`,
|
||||
timeoutMessage(command, args, this.deps.timeoutMs),
|
||||
command,
|
||||
args,
|
||||
null,
|
||||
@@ -106,7 +107,7 @@ export class QuerySubprocessAdapter {
|
||||
if (error.killed || (error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
||||
reject(
|
||||
this.deps.createToolsError(
|
||||
`gsd-tools timed out after ${this.deps.timeoutMs}ms: ${command} ${args.join(' ')}`,
|
||||
timeoutMessage(command, args, this.deps.timeoutMs),
|
||||
command,
|
||||
args,
|
||||
null,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GSDError, exitCodeFor } from './errors.js';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
import { errorMessage } from './query-failure-classification.js';
|
||||
|
||||
/**
|
||||
* Module owning projection of internal errors to GSDToolsError contract.
|
||||
@@ -16,7 +17,7 @@ export function toGSDToolsError(command: string, args: string[], err: unknown):
|
||||
);
|
||||
}
|
||||
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const msg = errorMessage(err);
|
||||
return new GSDToolsError(
|
||||
msg,
|
||||
command,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { QueryDispatchError, QueryDispatchResult } from './query-dispatch-contract.js';
|
||||
import { errorMessage, isTimeoutMessage, parseTimeoutMs } from '../query-failure-classification.js';
|
||||
import { fallbackFailureError, nativeFailureError, nativeTimeoutError } from './query-error-taxonomy.js';
|
||||
import { dispatchFailure } from './query-dispatch-result-builder.js';
|
||||
|
||||
@@ -10,21 +11,14 @@ export function toDispatchFailure(
|
||||
}
|
||||
|
||||
export function mapNativeDispatchError(error: unknown, command: string, args: string[]): QueryDispatchError {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (/timed out after/i.test(message)) {
|
||||
const message = errorMessage(error);
|
||||
if (isTimeoutMessage(message)) {
|
||||
return nativeTimeoutError({ message, command, args, timeoutMs: parseTimeoutMs(message) });
|
||||
}
|
||||
return nativeFailureError({ message, command, args });
|
||||
}
|
||||
|
||||
export function mapFallbackDispatchError(error: unknown, command: string, args: string[]): QueryDispatchError {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message = errorMessage(error);
|
||||
return fallbackFailureError({ message, command, args, backend: 'cjs' });
|
||||
}
|
||||
|
||||
function parseTimeoutMs(message: string): number | undefined {
|
||||
const m = message.match(/timed out after\s+(\d+)ms/i);
|
||||
if (!m) return undefined;
|
||||
const n = Number.parseInt(m[1], 10);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user