/ src / lib / shared-utils.ts
shared-utils.ts
 1  /**
 2   * Shared utility functions used across the codebase.
 3   *
 4   * These replace ad-hoc patterns that were duplicated in 100+ files:
 5   * - errorMessage: 231 occurrences across 112 files
 6   * - safeJsonParse: 137 occurrences across 82 files
 7   * - truncate: 62+ occurrences across 20+ files
 8   * - hmrSingleton: 77 occurrences across 38 files
 9   * - dedup/dedupBy: 40 occurrences across 28 files
10   * - sleep: 25 occurrences across 17 files
11   */
12  
13  /** Extract a human-readable error message from an unknown catch value. */
14  export function errorMessage(err: unknown): string {
15    return err instanceof Error ? err.message : String(err)
16  }
17  
18  /** Parse JSON with a fallback value instead of throwing. */
19  export function safeJsonParse<T>(json: string, fallback: T): T {
20    try {
21      return JSON.parse(json) as T
22    } catch {
23      return fallback
24    }
25  }
26  
27  /** Truncate a string to `limit` characters, optionally appending a suffix. */
28  export function truncate(s: string, limit: number, suffix = ''): string {
29    if (s.length <= limit) return s
30    const cutoff = Math.max(0, limit - suffix.length)
31    return s.slice(0, cutoff) + suffix
32  }
33  
34  /**
35   * HMR-safe singleton on globalThis. Survives Next.js hot module reloads.
36   * Replaces the ad-hoc `__swarmclaw_*` pattern scattered across 38 files.
37   */
38  export function hmrSingleton<T>(key: string, init: () => T): T {
39    const g = globalThis as Record<string, unknown>
40    return (g[key] ??= init()) as T
41  }
42  
43  /** Deduplicate an array preserving insertion order. */
44  export function dedup<T>(arr: T[]): T[] {
45    return [...new Set(arr)]
46  }
47  
48  /** Deduplicate an array by a key function, keeping the first occurrence. */
49  export function dedupBy<T>(arr: T[], key: (item: T) => string): T[] {
50    const seen = new Set<string>()
51    return arr.filter((item) => {
52      const k = key(item)
53      if (seen.has(k)) return false
54      seen.add(k)
55      return true
56    })
57  }
58  
59  /** Promise-based sleep. Replaces `await new Promise(r => setTimeout(r, ms))`. */
60  export function sleep(ms: number): Promise<void> {
61    return new Promise((resolve) => setTimeout(resolve, ms))
62  }
63  
64  /**
65   * Exponential backoff with 0-10% random jitter.
66   * Prevents thundering-herd when many callers retry simultaneously.
67   */
68  export function jitteredBackoff(baseMs: number, attempt: number, maxMs: number): number {
69    const exponential = Math.min(baseMs * Math.pow(2, attempt), maxMs)
70    const jitter = exponential * (Math.random() * 0.1)
71    return Math.round(exponential + jitter)
72  }