{"record":{"id":"f8731ccd743773b6","repo":"stablyai/orca","slug":"mobile-relay-host-overlay-storage-unreadable","errorCode":null,"errorMessage":"mobile relay host overlay storage unreadable","messagePattern":"mobile relay host overlay storage unreadable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mobile/src/transport/mobile-relay-host-overlay-store.ts","lineNumber":33,"sourceCode":"    const value = JSON.parse(raw) as unknown\n    if (!Array.isArray(value)) {\n      return null\n    }\n    return value.flatMap((item) => {\n      const result = MobileRelayHostOverlaySchema.safeParse(item)\n      return result.success ? [result.data] : []\n    })\n  } catch {\n    return null\n  }\n}\n\nasync function readOverlaysForMutation(): Promise<MobileRelayHostOverlay[]> {\n  const overlays = parseOverlays(await AsyncStorage.getItem(OVERLAY_STORAGE_KEY))\n  if (!overlays) {\n    // Why: never rewrite an unreadable v2 namespace as an empty list; doing so\n    // would destroy relay recovery data during an unrelated host mutation.\n    throw new Error('mobile relay host overlay storage unreadable')\n  }\n  return overlays\n}\n\nasync function mutateOverlays(\n  update: (overlays: MobileRelayHostOverlay[]) => MobileRelayHostOverlay[]\n): Promise<void> {\n  const mutation = overlayMutation.then(async () => {\n    const current = await readOverlaysForMutation()\n    const next = update(current)\n    // Why: direct-only saves commonly have no overlay to remove; avoid a full\n    // AsyncStorage write when cleanup leaves the durable list unchanged.\n    if (next !== current) {\n      await AsyncStorage.setItem(OVERLAY_STORAGE_KEY, JSON.stringify(next))\n    }\n  })\n  overlayMutation = mutation.catch(() => {})\n  return mutation","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/mobile-relay-host-overlay-store.ts#L15-L51","documentation":"Thrown by readOverlaysForMutation when parseOverlays returns null: AsyncStorage holds a value under 'orca:mobile-relay:host-overlays:v2' that is not a JSON array (corrupt JSON or a non-array shape). The guard deliberately refuses to rewrite an unreadable namespace as an empty list, to avoid destroying relay recovery data during an unrelated mutation.","triggerScenarios":"Any call to saveMobileRelayHostOverlay or removeMobileRelayHostOverlay(s) while the v2 key contains non-array JSON: a string, an object, or truncated JSON from a crash mid-write.","commonSituations":"A previous app version wrote a non-array under the same key; AsyncStorage corruption from a crash during setItem; manual injection during testing; a partial migration left legacy data in the v2 namespace.","solutions":["Read the raw value under the key (read-only) to confirm it is non-array/corrupt.","If genuinely unrecoverable, explicitly remove the key once, accepting that relay overlays will rebuild from host discovery.","Ensure every writer routes through MobileRelayHostOverlaySchema.parse so only valid v2 arrays are stored.","Add a one-time repair migration that detects non-array JSON and either quarantines or clears it deliberately."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"import AsyncStorage from '@react-native-async-storage/async-storage'\nasync function isOverlayStorageReadable(): Promise<boolean> {\n  const raw = await AsyncStorage.getItem('orca:mobile-relay:host-overlays:v2')\n  if (raw === null) return true\n  try {\n    return Array.isArray(JSON.parse(raw))\n  } catch {\n    return false\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await saveMobileRelayHostOverlay(overlay)\n} catch (error) {\n  if (error instanceof Error && error.message === 'mobile relay host overlay storage unreadable') {\n    // deliberate one-time repair: quarantine the bad key, then retry\n  }\n}","preventionTips":["Never write to the v2 overlay key except through MobileRelayHostOverlaySchema.parse-validated writers.","Add a startup probe that reads the key and reports unreadable state before any mutation is attempted.","Quarantine corrupt data into a separate key rather than silently clearing it."],"tags":["storage","async-storage","relay","data-integrity"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}