diegomura / react-pdf

📄 Create PDF files using React
https://react-pdf.org
MIT License
15.02k stars 1.19k forks source link

RangeError: Invalid array length at Array.push (<anonymous>) #2974

Open andremorescocia opened 1 week ago

andremorescocia commented 1 week ago

Describe the bug Updating to Version 4.x results in Error:

RangeError: Invalid array length at Array.push () at y (yoga-wasm-base64-esm.js:1273:1) at 00047d12:0xd3fc at 00047d12:0xb7e at 00047d12:0x9b62 at 00047d12:0xa5a5 at 00047d12:0x5a9c at 00047d12:0x769c at 00047d12:0xc3cf at 00047d12:0x76d7

The error occurs when generating a PDF with a very large volume of data (array.length > 5000). Even after taking more than 3 minutes, it eventually causes the error.

We tested using manual pagination, such as chunks, by defining the number of items per page, and it worked in a time 10 times faster. However, this solution is not viable because we cannot fix the number of items per page, as each generated PDF contains fields with variable sizes.

Expected behavior It is expected that the error mentioned above does not occur and that the PDF generation has better performance using its native pagination functions.

Desktop (please complete the following information):

diegomura commented 3 days ago

@andremorescocia can you provide a minimum reproducible case? This worked in version 3? Seems like yoga is hitting a limit. Nothing there changed recently

maciejkuran commented 2 days ago

Hey @diegomura, I hope you're doing okay :) I will bump up this thread because I get the same RangeError. Please, find below more details. Massive thanks in advance for your help.

Im using latest 4.1.5 version of @react-pdf/renderer. I tried to downgrade to lower versions, but nothing helped.

The error I get.

pdf.worker.ts:20 RangeError: Invalid array length
    at Array.push (<anonymous>)
    at y (yoga-wasm-base64-esm.js:63:248)
    at 00047d12:0xd3fc
    at 00047d12:0xb7e
    at 00047d12:0x9b62
    at 00047d12:0xa5a5
    at 00047d12:0x5a9c
    at 00047d12:0x769c
    at 00047d12:0xc3cf
    at 00047d12:0x76d7

The error above indicates an exceeding of limits with internal data structures in yoga - I assume.

It happens for super complex trees that I recursively render in PDF document - for less complex trees this render algo works very well as expected, enabling download. I am attaching a test tree.txt file (JS object), that actually produces this error log.

*UPDATE (uploaded object with the proper structure, ready for processing) tree.txt

I'm implementing a PDF download in Next.js app 14.2.3. The <ObjectItem/> is the component that produces the following error. It accepts the treeData through the props which is the tree.txt object I attached above.

Input data structure interface that I iterate through.

export interface TransformedNode {
  name: string;
  object_id: string;
  node_id?: string;
  work_id: string;
  object_type: string;
  object_subtype?: string;
  size: number;
  error?: string;
  total_size?: number;
  number_of_objects?: number;
  skipped_objects?: boolean;
  all_infected_symbols?: string[];
  infected_symbols?: string[];
  infected?: boolean;
  hashes: {
    sha256: string;
    [key: string]: any;
  };
  match_search?: boolean;
  children?: TransformedNode[] | [];
  ok?: {
    object_metadata: {
      name?: string;
      format?: string;
      natural_language?: string;
      programming_language?: string;
      nsfw_verdict?: string;
      [key: string]: any;
    };
    [key: string]: any;
  };
  relation_metadata: {
    name?: string;
    [key: string]: any;
  };
  navigation_nodes?: NavigationNode[];
  [key: string]: any;
}

Component code

import { View, Text } from '@react-pdf/renderer';

import { styles } from './ObjectItem.styles';

import { TransformedNode } from '@/app/(workspace)/analysis/_shared/types';

import { forbiddenObjectKeys } from '@/app/(workspace)/analysis/_shared/components/StackSection/AccordionItem';
import { globalPdfStyles } from '@/_shared/styles/globalPdfStyles';

interface ObjectItemProps {
  treeData: TransformedNode;
}

const ObjectItem = ({ treeData }: ObjectItemProps): JSX.Element => {
  const shouldRender = (value: any): boolean => {
    if (value === null || value === '' || value === undefined) return false;
    if (Array.isArray(value)) {
      return value.some(item => shouldRender(item));
    }
    if (typeof value === 'object' && Object.keys(value).length === 0) return false;
    return true;
  };

  const renderValue = (value: any, indentLevel: number): JSX.Element | string => {
    if (Array.isArray(value)) {
      return (
        <View style={[styles.arrayWrapper, { marginLeft: indentLevel * 2 }]}>
          {value.map((item, index) => (
            <View wrap={false} key={index}>
              {typeof item === 'object' ? (
                <View style={[styles.objectWrapper, { marginLeft: (indentLevel + 1) * 2 }]}>
                  {Object.entries(item)
                    .filter(([nestedKey, nestedValue]) => shouldRender(nestedValue))
                    .map(([nestedKey, nestedValue]) => (
                      <View wrap={false} key={nestedKey} style={styles.objectField}>
                        <Text style={globalPdfStyles.boldText}>{nestedKey.replace(/_/g, ' ')}</Text>
                        {renderValue(nestedValue, indentLevel + 2)}
                      </View>
                    ))}
                </View>
              ) : (
                <View wrap={false} style={styles.valueWrapper}>
                  <Text style={styles.value}>{item.toString()}</Text>
                </View>
              )}
            </View>
          ))}
        </View>
      );
    }

    if (typeof value === 'object' && value !== null) {
      return (
        <View style={[styles.objectWrapper, { marginLeft: indentLevel * 2 }]}>
          {Object.entries(value)
            .filter(([nestedKey, nestedValue]) => shouldRender(nestedValue))
            .map(([nestedKey, nestedValue]) => (
              <View key={nestedKey} style={styles.objectField}>
                <Text style={globalPdfStyles.boldText}>{nestedKey.replace(/_/g, ' ')}</Text>
                {renderValue(nestedValue, indentLevel + 1)}
              </View>
            ))}
        </View>
      );
    }

    //Known problem with word break NOT SUPPORTED. A workaround solution taken from -> https://github.com/diegomura/react-pdf/issues/248 e.g. to make long strings like sha... just word break
    return (
      <View wrap={false} style={styles.valueWrapper}>
        <Text style={styles.value}>
          {value
            ?.toString()
            .split('')
            .map((s: string, index: number) => {
              return <Text key={index}>{s}</Text>;
            }) || 'N/A'}
        </Text>
      </View>
    );
  };

  const renderObject = (obj: TransformedNode, indentLevel: number): JSX.Element[] => {
    return Object.entries(obj)
      .filter(
        ([key, value]) => ![...forbiddenObjectKeys, 'children'].includes(key) && shouldRender(value)
      )
      .map(([key, value]) => (
        <View key={key} style={[styles.objectField, { marginLeft: indentLevel * 2 }]}>
          <Text style={[globalPdfStyles.boldText, { color: '#0053D8' }]}>
            {key.replace(/_/g, ' ')}
          </Text>
          {renderValue(value, indentLevel + 1)}
        </View>
      ));
  };

  return (
    <View style={[styles.container]}>
      <View style={[styles.wrapper]}>
        <Text
          id={`object-${treeData.node_id}`}
          style={[globalPdfStyles.subHeading, { fontSize: 13 }]}
        >
          ({treeData.node_id}) {treeData.object_type}{' '}
          {treeData.object_subtype && `(${treeData.object_subtype})`}
        </Text>
        {renderObject(treeData, 0)}
      </View>
      {treeData.children?.map((child, index) => (
        <ObjectItem key={`${child.object_id}-${index}`} treeData={child} />
      ))}
    </View>
  );
};

export default ObjectItem;

Additionally attaching styles.

import { StyleSheet } from '@react-pdf/renderer';

export const styles = StyleSheet.create({
  container: {
    padding: 5,
  },
  wrapper: {
    marginBottom: 30,
  },
  objectField: {
    marginBottom: 5,
  },
  valueWrapper: {
    justifyContent: 'center',
    marginLeft: 5,
    padding: '3pt 10pt',
    border: '1pt solid #e7e7e7',
    borderRadius: 5,
  },
  value: {
    fontSize: 9,
    color: '#555',
  },
  arrayWrapper: {
    marginTop: 5,
    padding: 5,
    rowGap: 3,
  },
  objectWrapper: {
    marginTop: 5,
    borderLeft: '1pt solid #e7e7e7',
    padding: 5,
  },
});

Feel free @diegomura to ask more questions, but tbh I don't know what else I should add here. Thanks!