* wip * wip * wip * format * wip(mobile): lint and format * cleanup * nits * nits * idiomatic react * nit * format all root files
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
import { z } from 'zod';
|
|
import { appEnv } from '@/config/env';
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
message: string,
|
|
) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Thrown when a free-plan network attempts to create a non-container particle
|
|
* after hitting its daily message limit. Compose UI also disables triggers
|
|
* proactively via `useNetworkUsage` — this throw is a last-line defense.
|
|
*/
|
|
export class QuotaExceededError extends Error {
|
|
constructor(public readonly networkId: string) {
|
|
super('Daily message limit reached');
|
|
this.name = 'QuotaExceededError';
|
|
}
|
|
}
|
|
|
|
const IPC_PREFIX = /^Error invoking remote method '[^']+':\s*/;
|
|
|
|
function normalizeMessage(message: string): string {
|
|
return message
|
|
.replace(IPC_PREFIX, '')
|
|
.replace(/^Error:\s*/, '')
|
|
.trim();
|
|
}
|
|
|
|
export function toUserMessage(err: unknown): string {
|
|
if (err instanceof ApiError) {
|
|
if (err.status === 401) return 'Please sign in again.';
|
|
if (err.status === 403) return "You don't have permission to do that.";
|
|
if (err.status === 404) return 'Not found.';
|
|
if (err.status === 408 || err.status === 429) {
|
|
return 'Please try again in a moment.';
|
|
}
|
|
if (err.status >= 500) {
|
|
return 'Something went wrong on our end. Please try again.';
|
|
}
|
|
return normalizeMessage(err.message) || 'Request failed.';
|
|
}
|
|
|
|
if (err instanceof z.ZodError) {
|
|
return 'Received unexpected data from the server.';
|
|
}
|
|
|
|
if (err instanceof TypeError && /fetch|network/i.test(err.message)) {
|
|
return 'Network error. Check your connection.';
|
|
}
|
|
|
|
if (err instanceof Error) {
|
|
return normalizeMessage(err.message) || 'Something went wrong.';
|
|
}
|
|
|
|
return 'Something went wrong.';
|
|
}
|
|
|
|
type ErrorContext = Record<string, unknown>;
|
|
type ErrorSink = (err: unknown, context?: ErrorContext) => void;
|
|
|
|
// Sentry (or any observability backend) installs itself via `installErrorSinks`
|
|
// from renderer.tsx / main.ts. Until then, logError is a dev-only console call
|
|
// and reportError always prints — no call site needs to know.
|
|
let captureSink: ErrorSink | null = null;
|
|
let breadcrumbSink: ErrorSink | null = null;
|
|
|
|
export function installErrorSinks(sinks: {
|
|
capture: ErrorSink;
|
|
breadcrumb: ErrorSink;
|
|
}): void {
|
|
captureSink = sinks.capture;
|
|
breadcrumbSink = sinks.breadcrumb;
|
|
}
|
|
|
|
/** Expected-but-recordable failures. Breadcrumb only — never pages anyone. */
|
|
export function logError(err: unknown, context?: ErrorContext): void {
|
|
if (appEnv === 'dev') {
|
|
// eslint-disable-next-line no-console
|
|
console.error('[error]', err, context ?? {});
|
|
}
|
|
breadcrumbSink?.(err, context);
|
|
}
|
|
|
|
/** Unexpected failures the user may not see. Always captured. */
|
|
export function reportError(err: unknown, context?: ErrorContext): void {
|
|
// eslint-disable-next-line no-console
|
|
console.error('[error]', err, context ?? {});
|
|
captureSink?.(err, context);
|
|
}
|