Skip to content

Commit c0e9454

Browse files
committed
temp: PoC for merged document serialization/deserialization
1 parent 732bccb commit c0e9454

File tree

7 files changed

+660
-6
lines changed

7 files changed

+660
-6
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@
2929
"dependencies": {
3030
"@netcracker/qubership-apihub-api-unifier": "2.0.0",
3131
"@netcracker/qubership-apihub-json-crawl": "1.0.4",
32-
"fast-equals": "4.0.3"
32+
"fast-equals": "4.0.3",
33+
"flatted": "^3.2.9"
3334
},
3435
"devDependencies": {
3536
"@netcracker/qubership-apihub-compatibility-suites": "2.0.3",
3637
"@netcracker/qubership-apihub-graphapi": "1.0.8",
3738
"@netcracker/qubership-apihub-npm-gitflow": "3.1.0",
38-
"@types/jest": "29.5.11",
39+
"@types/jest": "29.5.11",
3940
"@types/node": "20.11.6",
4041
"@typescript-eslint/eslint-plugin": "6.13.2",
4142
"@typescript-eslint/parser": "6.13.2",

src/api.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
SpecType,
1717
} from '@netcracker/qubership-apihub-api-unifier'
1818
import { DEFAULT_NORMALIZED_RESULT, DEFAULT_OPTION_DEFAULTS_META_KEY, DEFAULT_OPTION_ORIGINS_META_KEY, DIFF_META_KEY } from './core'
19+
import { serialize, deserialize } from './utils'
1920

2021
export const COMPARE_ENGINES_MAP: Record<SpecType, CompareEngine> = {
2122
[SPEC_TYPE_JSON_SCHEMA_04]: compareJsonSchema(SPEC_TYPE_JSON_SCHEMA_04),
@@ -35,10 +36,16 @@ export function apiDiff(before: unknown, after: unknown, options: CompareOptions
3536
throw new Error(`Specification cannot be different. Got ${beforeSpec.type} and ${afterSpec.type}`)
3637
}
3738
const engine = COMPARE_ENGINES_MAP[beforeSpec.type]
38-
return engine(before, after, {
39+
40+
// Determine the metaKey to use (from options or default)
41+
const metaKey = options.metaKey || DIFF_META_KEY
42+
43+
// Time diff building
44+
const diffStartTime = performance.now()
45+
const result = engine(before, after, {
3946
mode: COMPARE_MODE_DEFAULT,
4047
normalizedResult: DEFAULT_NORMALIZED_RESULT,
41-
metaKey: DIFF_META_KEY,
48+
metaKey: metaKey,
4249
defaultsFlag: DEFAULT_OPTION_DEFAULTS_META_KEY,
4350
originsFlag: DEFAULT_OPTION_ORIGINS_META_KEY,
4451
compareScope: COMPARE_SCOPE_ROOT,
@@ -48,4 +55,34 @@ export function apiDiff(before: unknown, after: unknown, options: CompareOptions
4855
createdMergedJso: new Set(),
4956
...options,
5057
})
58+
const diffEndTime = performance.now()
59+
60+
// Serialize and deserialize the merged document, mapping metaKey to '__diff'
61+
const symbolToStringMapping = new Map<symbol, string>()
62+
symbolToStringMapping.set(metaKey, '__diff')
63+
64+
const stringToSymbolMapping = new Map<string, symbol>()
65+
stringToSymbolMapping.set('__diff', metaKey)
66+
67+
// Time serialization
68+
const serializeStartTime = performance.now()
69+
const serialized = serialize(result.merged, symbolToStringMapping)
70+
const serializeEndTime = performance.now()
71+
72+
// Time deserialization
73+
const deserializeStartTime = performance.now()
74+
const deserializedMerged = deserialize(serialized, stringToSymbolMapping)
75+
const deserializeEndTime = performance.now()
76+
77+
// Log all timings in one line
78+
const diffTime = diffEndTime - diffStartTime
79+
const serializeTime = serializeEndTime - serializeStartTime
80+
const deserializeTime = deserializeEndTime - deserializeStartTime
81+
const totalTime = deserializeEndTime - diffStartTime
82+
console.log(`ApiDiff timing - Diff: ${diffTime.toFixed(2)}ms, Serialize: ${serializeTime.toFixed(2)}ms, Deserialize: ${deserializeTime.toFixed(2)}ms, Total: ${totalTime.toFixed(2)}ms`)
83+
84+
return {
85+
...result,
86+
merged: deserializedMerged,
87+
}
5188
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ export {
2323
isDiffRemove,
2424
isDiffRename,
2525
isDiffReplace,
26+
serialize,
27+
deserialize,
2628
} from './utils'
2729
export { onlyExistedArrayIndexes } from './utils'

src/utils.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

2728
export 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+
}

test/json-schema.diff.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ describe('JSON schema changes', () => {
208208
expect(merged).not.toHaveProperty('writeOnly')
209209
})
210210

211+
// expected to fail in this PoC, we serialize/deserialize the merged document, but not the diffs
211212
it('diff share share instance', () => {
212213
const before = {
213214
type: 'object',

0 commit comments

Comments
 (0)