@@ -23,6 +23,7 @@ import {
2323 JSON_SCHEMA_NODE_TYPE_STRING ,
2424 JsonSchemaNodesNormalizedType ,
2525} from '@netcracker/qubership-apihub-api-unifier'
26+ import { stringify , parse } from 'flatted'
2627
2728export const isObject = ( value : unknown ) : value is Record < string | symbol , unknown > => {
2829 return typeof value === 'object' && value !== null
@@ -221,3 +222,169 @@ export const checkPrimitiveType = (value: unknown): PrimitiveType | undefined =>
221222 }
222223 return undefined
223224}
225+
226+ // This is just a PoC to serialize/deserialize the merged document.
227+ // Merged document is a JSON object with cycles and symbol keys.
228+ // Symbol key could be both in object and array.
229+ // flatted.stringify doesn't serialize custom properties on arrays
230+ // flatted.parse doesn't deserialize custom properties on arrays
231+ // lodash cloneDeepWith does not copy custom properties on arrays
232+ // Approximate times:
233+ // - OAS large x6: Diff: 57914.54ms, Serialize: 488.82ms, Deserialize: 1057.61ms
234+ // - GQL1: Diff: 25495.87ms, Serialize: 255.23ms, Deserialize: 477.04ms
235+ // - GQL2: Diff: 16599.96ms, Serialize: 341.62ms, Deserialize: 527.20ms
236+ // - Shopify (GQL): Diff: 21453.64ms, Serialize: 186.46ms, Deserialize: 371.04ms
237+ /**
238+ * Serializes an object with cycles and symbol substitution to a string
239+ * @param obj - The object to serialize (can contain cycles and Symbol keys)
240+ * @param symbolToStringMapping - Mapping from Symbol keys to string keys
241+ * @returns Serialized string representation
242+ */
243+ export const serialize = ( obj : unknown , symbolToStringMapping : Map < symbol , string > ) : string => {
244+
245+ // Walk the object and replace symbol keys, handling cycles
246+ const visited = new WeakSet ( )
247+
248+ const replaceSymbolKeys = ( object : any ) : any => {
249+ // Handle cycles by tracking visited objects
250+ if ( isObject ( object ) && visited . has ( object ) ) {
251+ return object
252+ }
253+
254+ if ( isObject ( object ) ) {
255+ visited . add ( object )
256+
257+ // Process symbol keys for both arrays and objects
258+ const symbolKeys = Object . getOwnPropertySymbols ( object )
259+ let hasSymbolKeys = false
260+ for ( const symbolKey of symbolKeys ) {
261+ const stringKey = symbolToStringMapping . get ( symbolKey )
262+ if ( stringKey ) {
263+ // Move value from symbol key to string key
264+ object [ stringKey ] = replaceSymbolKeys ( object [ symbolKey ] )
265+ delete object [ symbolKey ]
266+ hasSymbolKeys = true
267+ }
268+ }
269+
270+ if ( isArray ( object ) ) {
271+ // If array has symbol keys converted to string keys, we need to convert it to a plain object
272+ // because flatted.stringify doesn't serialize custom properties on arrays
273+ if ( hasSymbolKeys ) {
274+ const arrayAsObject : any = { __isArray : true }
275+ // Copy array elements
276+ for ( let i = 0 ; i < object . length ; i ++ ) {
277+ arrayAsObject [ i ] = replaceSymbolKeys ( object [ i ] )
278+ }
279+ // Copy any additional string properties (converted from symbols)
280+ for ( const [ key , value ] of Object . entries ( object ) ) {
281+ if ( ! ( / ^ \d + $ / . test ( key ) ) ) { // Skip numeric indices
282+ arrayAsObject [ key ] = replaceSymbolKeys ( value )
283+ }
284+ }
285+ arrayAsObject . length = object . length
286+ return arrayAsObject
287+ } else {
288+ // Process array elements normally
289+ for ( let i = 0 ; i < object . length ; i ++ ) {
290+ object [ i ] = replaceSymbolKeys ( object [ i ] )
291+ }
292+ }
293+ } else {
294+ // Process regular properties for objects
295+ for ( const [ key , objValue ] of Object . entries ( object ) ) {
296+ object [ key ] = replaceSymbolKeys ( objValue )
297+ }
298+ }
299+ }
300+
301+ return object
302+ }
303+
304+ const processedObj = replaceSymbolKeys ( obj )
305+ return stringify ( processedObj )
306+ }
307+
308+ /**
309+ * Deserializes a string back to an object with symbol key restoration
310+ * @param str - The serialized string
311+ * @param stringToSymbolMapping - Mapping from string keys to Symbol keys
312+ * @returns Deserialized object with Symbol keys restored
313+ */
314+ export const deserialize = ( str : string , stringToSymbolMapping : Map < string , symbol > ) : unknown => {
315+ // First, parse the string using flatted
316+ const parsedObj = parse ( str )
317+
318+ // Then walk the parsed object and replace string keys with symbol keys, handling cycles
319+ const visited = new WeakSet ( )
320+
321+ const replaceStringKeys = ( value : any ) : any => {
322+ // Handle cycles by tracking visited objects
323+ if ( isObject ( value ) && visited . has ( value ) ) {
324+ return value
325+ }
326+
327+ if ( isObject ( value ) ) {
328+ visited . add ( value )
329+
330+ // Check if this is a serialized array (converted to object during serialization)
331+ if ( value . __isArray === true ) {
332+ const arr : any [ ] = new Array ( value . length || 0 )
333+
334+ // Restore array elements
335+ for ( let i = 0 ; i < arr . length ; i ++ ) {
336+ if ( i in value ) {
337+ arr [ i ] = replaceStringKeys ( value [ i ] )
338+ }
339+ }
340+
341+ // Restore additional properties (including converted symbol keys)
342+ for ( const [ key , objValue ] of Object . entries ( value ) ) {
343+ if ( key !== '__isArray' && key !== 'length' && ! ( / ^ \d + $ / . test ( key ) ) ) {
344+ const symbolKey = stringToSymbolMapping . get ( key )
345+ if ( symbolKey ) {
346+ ( arr as any ) [ symbolKey ] = replaceStringKeys ( objValue )
347+ } else {
348+ ( arr as any ) [ key ] = replaceStringKeys ( objValue )
349+ }
350+ }
351+ }
352+
353+ return arr
354+ }
355+
356+ // Process string keys that should be converted to symbol keys for both arrays and objects
357+ const keysToReplace : Array < [ string , symbol ] > = [ ]
358+
359+ // First, identify which keys need to be replaced
360+ for ( const key of Object . keys ( value ) ) {
361+ const symbolKey = stringToSymbolMapping . get ( key )
362+ if ( symbolKey ) {
363+ keysToReplace . push ( [ key , symbolKey ] )
364+ }
365+ }
366+
367+ // Replace string keys with symbol keys
368+ for ( const [ stringKey , symbolKey ] of keysToReplace ) {
369+ value [ symbolKey ] = replaceStringKeys ( value [ stringKey ] )
370+ delete value [ stringKey ]
371+ }
372+
373+ if ( isArray ( value ) ) {
374+ // Process array elements
375+ for ( let i = 0 ; i < value . length ; i ++ ) {
376+ value [ i ] = replaceStringKeys ( value [ i ] )
377+ }
378+ } else {
379+ // Process remaining properties for objects
380+ for ( const [ key , objValue ] of Object . entries ( value ) ) {
381+ value [ key ] = replaceStringKeys ( objValue )
382+ }
383+ }
384+ }
385+
386+ return value
387+ }
388+
389+ return replaceStringKeys ( parsedObj )
390+ }
0 commit comments