Skip to content

Commit 6777fd4

Browse files
optimize validation with section memoization for improved performance
1 parent 37c0064 commit 6777fd4

File tree

1 file changed

+185
-57
lines changed

1 file changed

+185
-57
lines changed

src/components/schema-editor/schema-editor.component.tsx

Lines changed: 185 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, useCallback } from 'react';
1+
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
22
import AceEditor from 'react-ace';
33
import 'ace-builds/webpack-resolver';
44
import { addCompleter } from 'ace-builds/src-noconflict/ext-language_tools';
@@ -8,6 +8,7 @@ import { ActionableNotification, Link } from '@carbon/react';
88
import { useStandardFormSchema } from '@hooks/useStandardFormSchema';
99
import Ajv from 'ajv';
1010
import debounce from 'lodash-es/debounce';
11+
import isEqual from 'lodash-es/isEqual';
1112
import { ChevronRight, ChevronLeft } from '@carbon/react/icons';
1213
import styles from './schema-editor.scss';
1314

@@ -25,6 +26,13 @@ interface SchemaEditorProps {
2526
setValidationOn: (validationStatus: boolean) => void;
2627
}
2728

29+
// Interface for schema sections to track changes
30+
interface SchemaSection {
31+
path: string;
32+
content: any;
33+
errors?: Array<MarkerProps>;
34+
}
35+
2836
const SchemaEditor: React.FC<SchemaEditorProps> = ({
2937
onSchemaChange,
3038
stringifiedSchema,
@@ -40,6 +48,11 @@ const SchemaEditor: React.FC<SchemaEditorProps> = ({
4048
>([]);
4149
const [currentIndex, setCurrentIndex] = useState<number>(0);
4250

51+
// Store previous schema sections for comparison
52+
const [schemaSections, setSchemaSections] = useState<SchemaSection[]>([]);
53+
const ajvRef = useRef<Ajv | null>(null);
54+
const previousSchemaRef = useRef<string>('');
55+
4356
// Enable autocompletion in the schema
4457
const generateAutocompleteSuggestions = useCallback(() => {
4558
const suggestions: Array<{ name: string; type: string; path: string }> = [];
@@ -104,72 +117,187 @@ const SchemaEditor: React.FC<SchemaEditorProps> = ({
104117
});
105118
}, [autocompleteSuggestions]);
106119

107-
// Validate JSON schema
108-
const validateSchema = (content: string, schema) => {
120+
// Initialize Ajv instance once
121+
useEffect(() => {
122+
if (!ajvRef.current) {
123+
ajvRef.current = new Ajv({ allErrors: true, jsPropertySyntax: true, strict: false });
124+
}
125+
}, []);
126+
127+
// Extract schema sections for comparison
128+
const extractSchemaSections = useCallback((content: string): SchemaSection[] => {
109129
try {
110-
const trimmedContent = content.replace(/\s/g, '');
111-
// Check if the content is an empty object
112-
if (trimmedContent.trim() === '{}') {
113-
// Reset errors since the JSON is considered valid
114-
setErrors([]);
115-
return;
130+
const parsedContent = JSON.parse(content);
131+
const sections: SchemaSection[] = [];
132+
133+
// Extract top-level properties
134+
if (parsedContent.name) {
135+
sections.push({ path: 'name', content: parsedContent.name });
116136
}
117137

118-
const ajv = new Ajv({ allErrors: true, jsPropertySyntax: true, strict: false });
119-
const validate = ajv.compile(schema);
120-
const parsedContent = JSON.parse(content);
121-
const isValid = validate(parsedContent);
122-
const jsonLines = content.split('\n');
123-
124-
const traverse = (schemaPath) => {
125-
const pathSegments = schemaPath.split('/').filter((segment) => segment !== '' || segment !== 'type');
126-
let lineNumber = -1;
127-
128-
for (const segment of pathSegments) {
129-
if (segment === 'properties' || segment === 'items') continue; // Skip 'properties' and 'items'
130-
const match = segment.match(/^([^[\]]+)/); // Extract property key
131-
if (match) {
132-
const propertyName: string = pathSegments[pathSegments.length - 2]; // Get property key
133-
lineNumber = jsonLines.findIndex((line) => line.includes(propertyName));
138+
if (parsedContent.encounterType) {
139+
sections.push({ path: 'encounterType', content: parsedContent.encounterType });
140+
}
141+
142+
if (parsedContent.processor) {
143+
sections.push({ path: 'processor', content: parsedContent.processor });
144+
}
145+
146+
if (parsedContent.uuid) {
147+
sections.push({ path: 'uuid', content: parsedContent.uuid });
148+
}
149+
150+
// Extract pages and their sections
151+
if (parsedContent.pages && Array.isArray(parsedContent.pages)) {
152+
parsedContent.pages.forEach((page, pageIndex) => {
153+
sections.push({ path: `pages[${pageIndex}]`, content: page });
154+
155+
// Extract sections within pages
156+
if (page.sections && Array.isArray(page.sections)) {
157+
page.sections.forEach((section, sectionIndex) => {
158+
sections.push({
159+
path: `pages[${pageIndex}].sections[${sectionIndex}]`,
160+
content: section,
161+
});
162+
163+
// Extract questions within sections
164+
if (section.questions && Array.isArray(section.questions)) {
165+
section.questions.forEach((question, questionIndex) => {
166+
sections.push({
167+
path: `pages[${pageIndex}].sections[${sectionIndex}].questions[${questionIndex}]`,
168+
content: question,
169+
});
170+
});
171+
}
172+
});
134173
}
135-
if (lineNumber !== -1) break;
174+
});
175+
}
176+
177+
return sections;
178+
} catch (error) {
179+
console.error('Error parsing JSON for section extraction:', error);
180+
return [];
181+
}
182+
}, []);
183+
184+
// Validate JSON schema with memoization
185+
const validateSchema = useCallback(
186+
(content: string, schema) => {
187+
try {
188+
const trimmedContent = content.replace(/\s/g, '');
189+
// Check if the content is an empty object
190+
if (trimmedContent.trim() === '{}') {
191+
// Reset errors since the JSON is considered valid
192+
setErrors([]);
193+
return;
194+
}
195+
196+
// Parse the content
197+
const parsedContent = JSON.parse(content);
198+
199+
// Extract current schema sections
200+
const currentSections = extractSchemaSections(content);
201+
202+
// If schema hasn't changed, reuse previous validation results
203+
if (previousSchemaRef.current === content) {
204+
return;
136205
}
137206

138-
return lineNumber;
139-
};
140-
141-
if (!isValid) {
142-
const errorMarkers = validate.errors.map((error) => {
143-
const schemaPath = error.schemaPath.replace(/^#\//, ''); // Remove leading '#/'
144-
const lineNumber = traverse(schemaPath);
145-
const pathSegments = error.instancePath.split('.'); // Split the path into segments
146-
const errorPropertyName = pathSegments[pathSegments.length - 1];
147-
const message =
148-
error.keyword === 'type' || error.keyword === 'enum'
149-
? `${errorPropertyName.charAt(0).toUpperCase() + errorPropertyName.slice(1)} ${error.message}`
150-
: `${error.message.charAt(0).toUpperCase() + error.message.slice(1)}`;
151-
152-
return {
153-
startRow: lineNumber,
154-
startCol: 0,
155-
endRow: lineNumber,
156-
endCol: 1,
157-
className: 'error',
158-
text: message,
159-
type: 'text' as const,
160-
};
207+
// Compare with previous sections to find what changed
208+
const changedSections: SchemaSection[] = [];
209+
const unchangedSections: SchemaSection[] = [];
210+
211+
currentSections.forEach((currentSection) => {
212+
const previousSection = schemaSections.find((s) => s.path === currentSection.path);
213+
214+
if (!previousSection || !isEqual(previousSection.content, currentSection.content)) {
215+
changedSections.push(currentSection);
216+
} else {
217+
// Reuse previous validation results for unchanged sections
218+
unchangedSections.push(previousSection);
219+
}
161220
});
162221

163-
setErrors(errorMarkers);
164-
} else {
165-
setErrors([]);
222+
// If no Ajv instance, create one
223+
if (!ajvRef.current) {
224+
ajvRef.current = new Ajv({ allErrors: true, jsPropertySyntax: true, strict: false });
225+
}
226+
227+
const validate = ajvRef.current.compile(schema);
228+
const isValid = validate(parsedContent);
229+
const jsonLines = content.split('\n');
230+
231+
const traverse = (schemaPath) => {
232+
const pathSegments = schemaPath.split('/').filter((segment) => segment !== '' || segment !== 'type');
233+
let lineNumber = -1;
234+
235+
for (const segment of pathSegments) {
236+
if (segment === 'properties' || segment === 'items') continue; // Skip 'properties' and 'items'
237+
const match = segment.match(/^([^[\]]+)/); // Extract property key
238+
if (match) {
239+
const propertyName: string = pathSegments[pathSegments.length - 2]; // Get property key
240+
lineNumber = jsonLines.findIndex((line) => line.includes(propertyName));
241+
}
242+
if (lineNumber !== -1) break;
243+
}
244+
245+
return lineNumber;
246+
};
247+
248+
// Process validation errors
249+
let allErrors: MarkerProps[] = [];
250+
251+
if (!isValid && validate.errors) {
252+
// Group errors by path to associate them with sections
253+
const errorsByPath = validate.errors.reduce((acc, error) => {
254+
const path = error.instancePath || '/';
255+
if (!acc[path]) {
256+
acc[path] = [];
257+
}
258+
acc[path].push(error);
259+
return acc;
260+
}, {});
261+
262+
// Process errors for changed sections
263+
const newErrorMarkers = validate.errors.map((error) => {
264+
const schemaPath = error.schemaPath.replace(/^#\//, ''); // Remove leading '#/'
265+
const lineNumber = traverse(schemaPath);
266+
const pathSegments = error.instancePath.split('.'); // Split the path into segments
267+
const errorPropertyName = pathSegments[pathSegments.length - 1] || 'Schema';
268+
const message =
269+
error.keyword === 'type' || error.keyword === 'enum'
270+
? `${errorPropertyName.charAt(0).toUpperCase() + errorPropertyName.slice(1)} ${error.message}`
271+
: `${error.message.charAt(0).toUpperCase() + error.message.slice(1)}`;
272+
273+
return {
274+
startRow: lineNumber,
275+
startCol: 0,
276+
endRow: lineNumber,
277+
endCol: 1,
278+
className: 'error',
279+
text: message,
280+
type: 'text' as const,
281+
};
282+
});
283+
284+
allErrors = newErrorMarkers;
285+
}
286+
287+
// Update schema sections with new validation results
288+
setSchemaSections(currentSections);
289+
previousSchemaRef.current = content;
290+
291+
// Set all errors
292+
setErrors(allErrors);
293+
} catch (error) {
294+
console.error('Error parsing or validating JSON:', error);
166295
}
167-
} catch (error) {
168-
console.error('Error parsing or validating JSON:', error);
169-
}
170-
};
296+
},
297+
[extractSchemaSections, setErrors, schemaSections],
298+
);
171299

172-
const debouncedValidateSchema = debounce(validateSchema, 300);
300+
const debouncedValidateSchema = useMemo(() => debounce(validateSchema, 300), [validateSchema]);
173301

174302
const handleChange = (newValue: string) => {
175303
setValidationOn(false);

0 commit comments

Comments
 (0)