jexidb 2.1.9 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,778 +1,778 @@
1
- // NOTE: Buffer pool was removed due to complexity with low performance gain
2
- // It was causing serialization issues and data corruption in batch operations
3
- // If reintroducing buffer pooling in the future, ensure proper buffer management
4
- // and avoid reusing buffers that may contain stale data
5
-
6
- import SchemaManager from './SchemaManager.mjs'
7
-
8
- export default class Serializer {
9
- constructor(opts = {}) {
10
- this.opts = Object.assign({
11
- enableAdvancedSerialization: true,
12
- enableArraySerialization: true
13
- // NOTE: bufferPoolSize, adaptivePooling, memoryPressureThreshold removed
14
- // Buffer pool was causing more problems than benefits
15
- }, opts)
16
-
17
- // Initialize schema manager for array-based serialization
18
- this.schemaManager = new SchemaManager({
19
- enableArraySerialization: this.opts.enableArraySerialization,
20
- strictSchema: true,
21
- debugMode: this.opts.debugMode || false
22
- })
23
-
24
-
25
-
26
- // Advanced serialization settings
27
- this.serializationStats = {
28
- totalSerializations: 0,
29
- totalDeserializations: 0,
30
- jsonSerializations: 0,
31
- arraySerializations: 0,
32
- objectSerializations: 0
33
- }
34
- }
35
-
36
- /**
37
- * Initialize schema for array-based serialization
38
- */
39
- initializeSchema(schemaOrData, autoDetect = false) {
40
- this.schemaManager.initializeSchema(schemaOrData, autoDetect)
41
- }
42
-
43
- /**
44
- * Get current schema
45
- */
46
- getSchema() {
47
- return this.schemaManager.getSchema()
48
- }
49
-
50
- /**
51
- * Convert object to array format for optimized serialization
52
- */
53
- convertToArrayFormat(obj) {
54
- if (!this.opts.enableArraySerialization) {
55
- return obj
56
- }
57
- return this.schemaManager.objectToArray(obj)
58
- }
59
-
60
- /**
61
- * Convert array format back to object
62
- */
63
- convertFromArrayFormat(arr) {
64
- if (!this.opts.enableArraySerialization) {
65
- return arr
66
- }
67
- const obj = this.schemaManager.arrayToObject(arr)
68
-
69
- // CRITICAL FIX: Always preserve 'id' field if it exists in the original array
70
- // The 'id' field may not be in the schema but must be preserved
71
- // Check if array has more elements than schema fields - the extra element(s) might be the ID
72
- if (!obj.id && Array.isArray(arr) && this.schemaManager.isInitialized) {
73
- const schemaLength = this.schemaManager.schema ? this.schemaManager.schema.length : 0
74
- if (arr.length > schemaLength) {
75
- // Check if any extra element looks like an ID (string)
76
- for (let i = schemaLength; i < arr.length; i++) {
77
- const potentialId = arr[i]
78
- if (potentialId !== undefined && potentialId !== null && typeof potentialId === 'string' && potentialId.length > 0) {
79
- obj.id = potentialId
80
- break
81
- }
82
- }
83
- }
84
- }
85
-
86
- return obj
87
- }
88
-
89
- /**
90
- * Advanced serialization with optimized JSON and buffer pooling
91
- */
92
- serialize(data, opts = {}) {
93
- this.serializationStats.totalSerializations++
94
- const addLinebreak = opts.linebreak !== false
95
-
96
- // Convert to array format if enabled
97
- const serializationData = this.convertToArrayFormat(data)
98
-
99
- // Track conversion statistics
100
- if (Array.isArray(serializationData) && typeof data === 'object' && data !== null) {
101
- this.serializationStats.arraySerializations++
102
- } else {
103
- this.serializationStats.objectSerializations++
104
- }
105
-
106
- // Use advanced JSON serialization
107
- if (this.opts.enableAdvancedSerialization) {
108
- this.serializationStats.jsonSerializations++
109
- return this.serializeAdvanced(serializationData, addLinebreak)
110
- }
111
-
112
- // Fallback to standard serialization
113
- this.serializationStats.jsonSerializations++
114
- return this.serializeStandard(serializationData, addLinebreak)
115
- }
116
-
117
-
118
-
119
- /**
120
- * Advanced serialization with optimized JSON.stringify and buffer pooling
121
- */
122
- serializeAdvanced(data, addLinebreak) {
123
- // CRITICAL FIX: Sanitize data to remove problematic characters before serialization
124
- const sanitizedData = this.sanitizeDataForJSON(data)
125
-
126
- // Validate encoding before serialization
127
- this.validateEncodingBeforeSerialization(sanitizedData)
128
-
129
- // Use optimized JSON.stringify without buffer pooling
130
- // NOTE: Buffer pool removed - using direct Buffer creation for simplicity and reliability
131
- const json = this.optimizedStringify(sanitizedData)
132
-
133
- // CRITICAL FIX: Normalize encoding before creating buffer
134
- const normalizedJson = this.normalizeEncoding(json)
135
- const jsonBuffer = Buffer.from(normalizedJson, 'utf8')
136
-
137
- const totalLength = jsonBuffer.length + (addLinebreak ? 1 : 0)
138
- const result = Buffer.allocUnsafe(totalLength)
139
-
140
- jsonBuffer.copy(result, 0, 0, jsonBuffer.length)
141
- if (addLinebreak) {
142
- result[jsonBuffer.length] = 0x0A
143
- }
144
-
145
- return result
146
- }
147
-
148
- /**
149
- * Proper encoding normalization with UTF-8 validation
150
- * Fixed to prevent double-encoding and data corruption
151
- */
152
- normalizeEncoding(str) {
153
- if (typeof str !== 'string') return str
154
-
155
- // Skip if already valid UTF-8 (99% of cases)
156
- if (this.isValidUTF8(str)) return str
157
-
158
- // Try to detect and convert encoding safely
159
- return this.safeConvertToUTF8(str)
160
- }
161
-
162
- /**
163
- * Check if string is valid UTF-8
164
- */
165
- isValidUTF8(str) {
166
- try {
167
- // Test if string can be encoded and decoded as UTF-8 without loss
168
- const encoded = Buffer.from(str, 'utf8')
169
- const decoded = encoded.toString('utf8')
170
- return decoded === str
171
- } catch (error) {
172
- return false
173
- }
174
- }
175
-
176
- /**
177
- * Safe conversion to UTF-8 with proper encoding detection
178
- */
179
- safeConvertToUTF8(str) {
180
- // Try common encodings in order of likelihood
181
- const encodings = ['utf8', 'latin1', 'utf16le', 'ascii']
182
-
183
- for (const encoding of encodings) {
184
- try {
185
- const converted = Buffer.from(str, encoding).toString('utf8')
186
-
187
- // Validate the conversion didn't lose information
188
- if (this.isValidUTF8(converted)) {
189
- return converted
190
- }
191
- } catch (error) {
192
- // Try next encoding
193
- continue
194
- }
195
- }
196
-
197
- // Fallback: return original string (preserve data)
198
- console.warn('JexiDB: Could not normalize encoding, preserving original string')
199
- return str
200
- }
201
-
202
- /**
203
- * Enhanced deep encoding normalization with UTF-8 validation
204
- * Fixed to prevent double-encoding and data corruption
205
- */
206
- deepNormalizeEncoding(obj) {
207
- if (obj === null || obj === undefined) return obj
208
-
209
- if (typeof obj === 'string') {
210
- return this.normalizeEncoding(obj)
211
- }
212
-
213
- if (Array.isArray(obj)) {
214
- // Check if normalization is needed (performance optimization)
215
- const needsNormalization = obj.some(item =>
216
- typeof item === 'string' && !this.isValidUTF8(item)
217
- )
218
-
219
- if (!needsNormalization) return obj
220
-
221
- return obj.map(item => this.deepNormalizeEncoding(item))
222
- }
223
-
224
- if (typeof obj === 'object') {
225
- // Check if normalization is needed (performance optimization)
226
- const needsNormalization = Object.values(obj).some(value =>
227
- typeof value === 'string' && !this.isValidUTF8(value)
228
- )
229
-
230
- if (!needsNormalization) return obj
231
-
232
- const normalized = {}
233
- for (const [key, value] of Object.entries(obj)) {
234
- normalized[key] = this.deepNormalizeEncoding(value)
235
- }
236
- return normalized
237
- }
238
-
239
- return obj
240
- }
241
-
242
- /**
243
- * Validate encoding before serialization
244
- */
245
- /**
246
- * Sanitize data to remove problematic characters that break JSON parsing
247
- * CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors
248
- * by removing control characters that cannot be safely represented in JSON
249
- */
250
- sanitizeDataForJSON(data) {
251
- const sanitizeString = (str) => {
252
- if (typeof str !== 'string') return str
253
-
254
- return str
255
- // Remove control characters that break JSON parsing (but keep \n, \r, \t as they can be escaped)
256
- // Remove: NUL, SOH, STX, ETX, EOT, ENQ, ACK, BEL, VT, FF, SO, SI, DLE, DC1-DC4, NAK, SYN, ETB, CAN, EM, SUB, ESC, FS, GS, RS, US, DEL, C1 controls
257
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
258
- // Limit string length to prevent performance issues
259
- .substring(0, 10000)
260
- }
261
-
262
- const sanitizeArray = (arr) => {
263
- if (!Array.isArray(arr)) return arr
264
-
265
- return arr
266
- .map(item => this.sanitizeDataForJSON(item))
267
- .filter(item => item !== null && item !== undefined && item !== '')
268
- }
269
-
270
- if (typeof data === 'string') {
271
- return sanitizeString(data)
272
- }
273
-
274
- if (Array.isArray(data)) {
275
- return sanitizeArray(data)
276
- }
277
-
278
- if (data && typeof data === 'object') {
279
- const sanitized = {}
280
- for (const [key, value] of Object.entries(data)) {
281
- const sanitizedValue = this.sanitizeDataForJSON(value)
282
- // Only include non-null, non-undefined values
283
- if (sanitizedValue !== null && sanitizedValue !== undefined) {
284
- sanitized[key] = sanitizedValue
285
- }
286
- }
287
- return sanitized
288
- }
289
-
290
- return data
291
- }
292
-
293
- validateEncodingBeforeSerialization(data) {
294
- const issues = []
295
-
296
- const checkString = (str, path = '') => {
297
- if (typeof str === 'string' && !this.isValidUTF8(str)) {
298
- issues.push(`Invalid encoding at ${path}: "${str.substring(0, 50)}..."`)
299
- }
300
- }
301
-
302
- const traverse = (obj, path = '') => {
303
- if (typeof obj === 'string') {
304
- checkString(obj, path)
305
- } else if (Array.isArray(obj)) {
306
- obj.forEach((item, index) => {
307
- traverse(item, `${path}[${index}]`)
308
- })
309
- } else if (obj && typeof obj === 'object') {
310
- Object.entries(obj).forEach(([key, value]) => {
311
- traverse(value, path ? `${path}.${key}` : key)
312
- })
313
- }
314
- }
315
-
316
- traverse(data)
317
-
318
- if (issues.length > 0) {
319
- console.warn('JexiDB: Encoding issues detected:', issues)
320
- }
321
-
322
- return issues.length === 0
323
- }
324
-
325
- /**
326
- * Optimized JSON.stringify with fast paths for common data structures
327
- * Now includes deep encoding normalization for all string fields
328
- */
329
- optimizedStringify(obj) {
330
- // CRITICAL: Normalize encoding for all string fields before stringify
331
- const normalizedObj = this.deepNormalizeEncoding(obj)
332
- return this._stringifyNormalizedValue(normalizedObj)
333
- }
334
-
335
- _stringifyNormalizedValue(value) {
336
- // Fast path for null and undefined
337
- if (value === null || value === undefined) {
338
- return 'null'
339
- }
340
-
341
- const type = typeof value
342
-
343
- // Fast path for primitives
344
- if (type === 'boolean') {
345
- return value ? 'true' : 'false'
346
- }
347
-
348
- if (type === 'number') {
349
- return Number.isFinite(value) ? value.toString() : 'null'
350
- }
351
-
352
- if (type === 'string') {
353
- // Fast path for simple strings (no escaping needed)
354
- if (!/[\\"\u0000-\u001f]/.test(value)) {
355
- return '"' + value + '"'
356
- }
357
- // Fall back to JSON.stringify for complex strings
358
- return JSON.stringify(value)
359
- }
360
-
361
- if (Array.isArray(value)) {
362
- return this._stringifyNormalizedArray(value)
363
- }
364
-
365
- if (type === 'object') {
366
- const keys = Object.keys(value)
367
- if (keys.length === 0) return '{}'
368
- // Use native stringify for object to leverage stable handling of undefined, Dates, etc.
369
- return JSON.stringify(value)
370
- }
371
-
372
- // Fallback to JSON.stringify for unknown types (BigInt, symbols, etc.)
373
- return JSON.stringify(value)
374
- }
375
-
376
- _stringifyNormalizedArray(arr) {
377
- const length = arr.length
378
- if (length === 0) return '[]'
379
-
380
- let result = '['
381
- for (let i = 0; i < length; i++) {
382
- if (i > 0) result += ','
383
- const element = arr[i]
384
-
385
- // JSON spec: undefined, functions, and symbols are serialized as null within arrays
386
- if (element === undefined || typeof element === 'function' || typeof element === 'symbol') {
387
- result += 'null'
388
- continue
389
- }
390
-
391
- result += this._stringifyNormalizedValue(element)
392
- }
393
- result += ']'
394
- return result
395
- }
396
-
397
- /**
398
- * Standard serialization (fallback)
399
- */
400
- serializeStandard(data, addLinebreak) {
401
- // CRITICAL FIX: Sanitize data to remove problematic characters before serialization
402
- const sanitizedData = this.sanitizeDataForJSON(data)
403
-
404
- // Validate encoding before serialization
405
- this.validateEncodingBeforeSerialization(sanitizedData)
406
-
407
- // NOTE: Buffer pool removed - using direct Buffer creation for simplicity and reliability
408
- // CRITICAL: Normalize encoding for all string fields before stringify
409
- const normalizedData = this.deepNormalizeEncoding(sanitizedData)
410
- const json = JSON.stringify(normalizedData)
411
-
412
- // CRITICAL FIX: Normalize encoding before creating buffer
413
- const normalizedJson = this.normalizeEncoding(json)
414
- const jsonBuffer = Buffer.from(normalizedJson, 'utf8')
415
-
416
- const totalLength = jsonBuffer.length + (addLinebreak ? 1 : 0)
417
- const result = Buffer.allocUnsafe(totalLength)
418
-
419
- jsonBuffer.copy(result, 0, 0, jsonBuffer.length)
420
- if (addLinebreak) {
421
- result[jsonBuffer.length] = 0x0A
422
- }
423
-
424
- return result
425
- }
426
-
427
- /**
428
- * Advanced deserialization with fast paths
429
- */
430
- deserialize(data) {
431
- this.serializationStats.totalDeserializations++
432
-
433
- if (data.length === 0) return null
434
-
435
- try {
436
- // Handle both Buffer and string inputs
437
- let str
438
- if (Buffer.isBuffer(data)) {
439
- // Fast path: avoid toString() for empty data
440
- if (data.length === 1 && data[0] === 0x0A) return null // Just newline
441
- str = data.toString('utf8').trim()
442
- } else if (typeof data === 'string') {
443
- str = data.trim()
444
- } else {
445
- throw new Error('Invalid data type for deserialization')
446
- }
447
-
448
- const strLength = str.length
449
-
450
- // Fast path for empty strings
451
- if (strLength === 0) return null
452
-
453
- // CRITICAL FIX: Detect and handle multiple JSON objects in the same line
454
- // This can happen if data was corrupted during concurrent writes or offset calculation errors
455
- const firstBrace = str.indexOf('{')
456
- const firstBracket = str.indexOf('[')
457
-
458
- // Helper function to extract first complete JSON object/array from a string
459
- // CRITICAL FIX: Must handle strings and escaped characters correctly
460
- // to avoid counting braces/brackets that are inside string values
461
- const extractFirstJson = (jsonStr, startChar) => {
462
- if (startChar === '{') {
463
- let braceCount = 0
464
- let endPos = -1
465
- let inString = false
466
- let escapeNext = false
467
-
468
- for (let i = 0; i < jsonStr.length; i++) {
469
- const char = jsonStr[i]
470
-
471
- if (escapeNext) {
472
- escapeNext = false
473
- continue
474
- }
475
-
476
- if (char === '\\') {
477
- escapeNext = true
478
- continue
479
- }
480
-
481
- if (char === '"' && !escapeNext) {
482
- inString = !inString
483
- continue
484
- }
485
-
486
- if (!inString) {
487
- if (char === '{') braceCount++
488
- if (char === '}') {
489
- braceCount--
490
- if (braceCount === 0) {
491
- endPos = i + 1
492
- break
493
- }
494
- }
495
- }
496
- }
497
- return endPos > 0 ? jsonStr.substring(0, endPos) : null
498
- } else if (startChar === '[') {
499
- let bracketCount = 0
500
- let endPos = -1
501
- let inString = false
502
- let escapeNext = false
503
-
504
- for (let i = 0; i < jsonStr.length; i++) {
505
- const char = jsonStr[i]
506
-
507
- if (escapeNext) {
508
- escapeNext = false
509
- continue
510
- }
511
-
512
- if (char === '\\') {
513
- escapeNext = true
514
- continue
515
- }
516
-
517
- if (char === '"' && !escapeNext) {
518
- inString = !inString
519
- continue
520
- }
521
-
522
- if (!inString) {
523
- if (char === '[') bracketCount++
524
- if (char === ']') {
525
- bracketCount--
526
- if (bracketCount === 0) {
527
- endPos = i + 1
528
- break
529
- }
530
- }
531
- }
532
- }
533
- return endPos > 0 ? jsonStr.substring(0, endPos) : null
534
- }
535
- return null
536
- }
537
-
538
- // Check if JSON starts at the beginning of the string
539
- const jsonStartsAtZero = (firstBrace === 0) || (firstBracket === 0)
540
- let hasValidJson = false
541
-
542
- if (jsonStartsAtZero) {
543
- // JSON starts at beginning - check for multiple JSON objects/arrays
544
- if (firstBrace === 0) {
545
- const secondBrace = str.indexOf('{', 1)
546
- if (secondBrace !== -1) {
547
- // Multiple objects detected - extract first
548
- const extracted = extractFirstJson(str, '{')
549
- if (extracted) {
550
- str = extracted
551
- hasValidJson = true
552
- if (this.opts && this.opts.debugMode) {
553
- console.warn(`⚠️ Deserialize: Multiple JSON objects detected, using first object only`)
554
- }
555
- }
556
- } else {
557
- hasValidJson = true // Single valid object starting at 0
558
- }
559
- } else if (firstBracket === 0) {
560
- const secondBracket = str.indexOf('[', 1)
561
- if (secondBracket !== -1) {
562
- // Multiple arrays detected - extract first
563
- const extracted = extractFirstJson(str, '[')
564
- if (extracted) {
565
- str = extracted
566
- hasValidJson = true
567
- if (this.opts && this.opts.debugMode) {
568
- console.warn(`⚠️ Deserialize: Multiple JSON arrays detected, using first array only`)
569
- }
570
- }
571
- } else {
572
- hasValidJson = true // Single valid array starting at 0
573
- }
574
- }
575
- } else {
576
- // JSON doesn't start at beginning - try to find and extract first valid JSON
577
- const jsonStart = firstBrace !== -1 ? (firstBracket !== -1 ? Math.min(firstBrace, firstBracket) : firstBrace) : firstBracket
578
-
579
- if (jsonStart !== -1 && jsonStart > 0) {
580
- // Found JSON but not at start - extract from that position
581
- const jsonStr = str.substring(jsonStart)
582
- const startChar = jsonStr[0]
583
- const extracted = extractFirstJson(jsonStr, startChar)
584
-
585
- if (extracted) {
586
- str = extracted
587
- hasValidJson = true
588
- if (this.opts && this.opts.debugMode) {
589
- console.warn(`⚠️ Deserialize: Found JSON after ${jsonStart} chars of invalid text, extracted first ${startChar === '{' ? 'object' : 'array'}`)
590
- }
591
- }
592
- }
593
- }
594
-
595
- // CRITICAL FIX: If no valid JSON structure found, throw error before attempting parse
596
- // This allows walk() and other callers to catch and skip invalid lines
597
- if (!hasValidJson && firstBrace === -1 && firstBracket === -1) {
598
- const errorStr = Buffer.isBuffer(data) ? data.toString('utf8').trim() : data.trim()
599
- const error = new Error(`Failed to deserialize JSON data: No valid JSON structure found in "${errorStr.substring(0, 100)}..."`)
600
- // Mark this as a "no valid JSON" error so it can be handled appropriately
601
- error.noValidJson = true
602
- throw error
603
- }
604
-
605
- // If we tried to extract but got nothing valid, also throw error
606
- if (hasValidJson && (!str || str.trim().length === 0)) {
607
- const error = new Error(`Failed to deserialize JSON data: Extracted JSON is empty`)
608
- error.noValidJson = true
609
- throw error
610
- }
611
-
612
- // Parse JSON data
613
- const parsedData = JSON.parse(str)
614
-
615
- // Convert from array format back to object if needed
616
- return this.convertFromArrayFormat(parsedData)
617
- } catch (e) {
618
- // If error was already formatted with noValidJson flag, re-throw as-is
619
- if (e.noValidJson) {
620
- throw e
621
- }
622
- // Otherwise, format the error message
623
- const str = Buffer.isBuffer(data) ? data.toString('utf8').trim() : data.trim()
624
- throw new Error(`Failed to deserialize JSON data: "${str.substring(0, 100)}..." - ${e.message}`)
625
- }
626
- }
627
-
628
- /**
629
- * Batch serialization for multiple records
630
- */
631
- serializeBatch(dataArray, opts = {}) {
632
- // CRITICAL FIX: Sanitize data to remove problematic characters before serialization
633
- const sanitizedDataArray = dataArray.map(data => this.sanitizeDataForJSON(data))
634
-
635
- // Validate encoding before serialization
636
- this.validateEncodingBeforeSerialization(sanitizedDataArray)
637
-
638
- // Convert all objects to array format for optimization
639
- const convertedData = sanitizedDataArray.map(data => this.convertToArrayFormat(data))
640
-
641
- // Track conversion statistics
642
- this.serializationStats.arraySerializations += convertedData.filter((item, index) =>
643
- Array.isArray(item) && typeof dataArray[index] === 'object' && dataArray[index] !== null
644
- ).length
645
- this.serializationStats.objectSerializations += dataArray.length - this.serializationStats.arraySerializations
646
-
647
- // JSONL format: serialize each array as a separate line
648
- try {
649
- const lines = []
650
- for (const arrayData of convertedData) {
651
- const json = this.optimizedStringify(arrayData)
652
- const normalizedJson = this.normalizeEncoding(json)
653
- lines.push(normalizedJson)
654
- }
655
-
656
- // Join all lines with newlines
657
- const jsonlContent = lines.join('\n')
658
- const jsonlBuffer = Buffer.from(jsonlContent, 'utf8')
659
-
660
- // Add final linebreak if requested
661
- const addLinebreak = opts.linebreak !== false
662
- const totalLength = jsonlBuffer.length + (addLinebreak ? 1 : 0)
663
- const result = Buffer.allocUnsafe(totalLength)
664
-
665
- jsonlBuffer.copy(result, 0, 0, jsonlBuffer.length)
666
- if (addLinebreak) {
667
- result[jsonlBuffer.length] = 0x0A
668
- }
669
-
670
- return result
671
- } catch (e) {
672
- // Fallback to individual serialization if batch serialization fails
673
- const results = []
674
- const batchSize = opts.batchSize || 100
675
-
676
- for (let i = 0; i < convertedData.length; i += batchSize) {
677
- const batch = convertedData.slice(i, i + batchSize)
678
- const batchResults = batch.map(data => this.serialize(data, opts))
679
- results.push(...batchResults)
680
- }
681
-
682
- return results
683
- }
684
- }
685
-
686
- /**
687
- * Batch deserialization for multiple records
688
- */
689
- deserializeBatch(dataArray) {
690
- // Optimization: try to parse all entries as a single JSON array first
691
- // This is much faster than parsing each entry individually
692
- try {
693
- // Convert all entries to strings and join them as a single JSON array
694
- const entriesJson = '[' + dataArray.map(data => {
695
- if (Buffer.isBuffer(data)) {
696
- return data.toString('utf8').trim()
697
- } else if (typeof data === 'string') {
698
- return data.trim()
699
- } else {
700
- throw new Error('Invalid data type for batch deserialization')
701
- }
702
- }).join(',') + ']'
703
- const parsedResults = JSON.parse(entriesJson)
704
-
705
- // Convert arrays back to objects if needed
706
- const results = parsedResults.map(data => this.convertFromArrayFormat(data))
707
-
708
- // Validate that all results are objects (JexiDB requirement)
709
- if (Array.isArray(results) && results.every(item => item && typeof item === 'object')) {
710
- return results
711
- }
712
-
713
- // If validation fails, fall back to individual parsing
714
- throw new Error('Validation failed - not all entries are objects')
715
- } catch (e) {
716
- // Fallback to individual deserialization if batch parsing fails
717
- const results = []
718
- const batchSize = 100 // Process in batches to avoid blocking
719
-
720
- for (let i = 0; i < dataArray.length; i += batchSize) {
721
- const batch = dataArray.slice(i, i + batchSize)
722
- const batchResults = batch.map(data => this.deserialize(data))
723
- results.push(...batchResults)
724
- }
725
-
726
- return results
727
- }
728
- }
729
-
730
- /**
731
- * Check if data appears to be binary (always false since we only use JSON now)
732
- */
733
- isBinaryData(data) {
734
- // All data is now JSON format
735
- return false
736
- }
737
-
738
- /**
739
- * Get comprehensive performance statistics
740
- */
741
- getStats() {
742
- // NOTE: Buffer pool stats removed - buffer pool was causing more problems than benefits
743
- return {
744
- // Serialization stats
745
- totalSerializations: this.serializationStats.totalSerializations,
746
- totalDeserializations: this.serializationStats.totalDeserializations,
747
- jsonSerializations: this.serializationStats.jsonSerializations,
748
- arraySerializations: this.serializationStats.arraySerializations,
749
- objectSerializations: this.serializationStats.objectSerializations,
750
-
751
- // Configuration
752
- enableAdvancedSerialization: this.opts.enableAdvancedSerialization,
753
- enableArraySerialization: this.opts.enableArraySerialization,
754
-
755
- // Schema stats
756
- schemaStats: this.schemaManager.getStats()
757
- }
758
- }
759
-
760
- /**
761
- * Cleanup resources
762
- */
763
- cleanup() {
764
- // NOTE: Buffer pool cleanup removed - buffer pool was causing more problems than benefits
765
- this.serializationStats = {
766
- totalSerializations: 0,
767
- totalDeserializations: 0,
768
- jsonSerializations: 0,
769
- arraySerializations: 0,
770
- objectSerializations: 0
771
- }
772
-
773
- // Reset schema manager
774
- if (this.schemaManager) {
775
- this.schemaManager.reset()
776
- }
777
- }
1
+ // NOTE: Buffer pool was removed due to complexity with low performance gain
2
+ // It was causing serialization issues and data corruption in batch operations
3
+ // If reintroducing buffer pooling in the future, ensure proper buffer management
4
+ // and avoid reusing buffers that may contain stale data
5
+
6
+ import SchemaManager from './SchemaManager.mjs'
7
+
8
+ export default class Serializer {
9
+ constructor(opts = {}) {
10
+ this.opts = Object.assign({
11
+ enableAdvancedSerialization: true,
12
+ enableArraySerialization: true
13
+ // NOTE: bufferPoolSize, adaptivePooling, memoryPressureThreshold removed
14
+ // Buffer pool was causing more problems than benefits
15
+ }, opts)
16
+
17
+ // Initialize schema manager for array-based serialization
18
+ this.schemaManager = new SchemaManager({
19
+ enableArraySerialization: this.opts.enableArraySerialization,
20
+ strictSchema: true,
21
+ debugMode: this.opts.debugMode || false
22
+ })
23
+
24
+
25
+
26
+ // Advanced serialization settings
27
+ this.serializationStats = {
28
+ totalSerializations: 0,
29
+ totalDeserializations: 0,
30
+ jsonSerializations: 0,
31
+ arraySerializations: 0,
32
+ objectSerializations: 0
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Initialize schema for array-based serialization
38
+ */
39
+ initializeSchema(schemaOrData, autoDetect = false) {
40
+ this.schemaManager.initializeSchema(schemaOrData, autoDetect)
41
+ }
42
+
43
+ /**
44
+ * Get current schema
45
+ */
46
+ getSchema() {
47
+ return this.schemaManager.getSchema()
48
+ }
49
+
50
+ /**
51
+ * Convert object to array format for optimized serialization
52
+ */
53
+ convertToArrayFormat(obj) {
54
+ if (!this.opts.enableArraySerialization) {
55
+ return obj
56
+ }
57
+ return this.schemaManager.objectToArray(obj)
58
+ }
59
+
60
+ /**
61
+ * Convert array format back to object
62
+ */
63
+ convertFromArrayFormat(arr) {
64
+ if (!this.opts.enableArraySerialization) {
65
+ return arr
66
+ }
67
+ const obj = this.schemaManager.arrayToObject(arr)
68
+
69
+ // CRITICAL FIX: Always preserve 'id' field if it exists in the original array
70
+ // The 'id' field may not be in the schema but must be preserved
71
+ // Check if array has more elements than schema fields - the extra element(s) might be the ID
72
+ if (!obj.id && Array.isArray(arr) && this.schemaManager.isInitialized) {
73
+ const schemaLength = this.schemaManager.schema ? this.schemaManager.schema.length : 0
74
+ if (arr.length > schemaLength) {
75
+ // Check if any extra element looks like an ID (string)
76
+ for (let i = schemaLength; i < arr.length; i++) {
77
+ const potentialId = arr[i]
78
+ if (potentialId !== undefined && potentialId !== null && typeof potentialId === 'string' && potentialId.length > 0) {
79
+ obj.id = potentialId
80
+ break
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ return obj
87
+ }
88
+
89
+ /**
90
+ * Advanced serialization with optimized JSON and buffer pooling
91
+ */
92
+ serialize(data, opts = {}) {
93
+ this.serializationStats.totalSerializations++
94
+ const addLinebreak = opts.linebreak !== false
95
+
96
+ // Convert to array format if enabled
97
+ const serializationData = this.convertToArrayFormat(data)
98
+
99
+ // Track conversion statistics
100
+ if (Array.isArray(serializationData) && typeof data === 'object' && data !== null) {
101
+ this.serializationStats.arraySerializations++
102
+ } else {
103
+ this.serializationStats.objectSerializations++
104
+ }
105
+
106
+ // Use advanced JSON serialization
107
+ if (this.opts.enableAdvancedSerialization) {
108
+ this.serializationStats.jsonSerializations++
109
+ return this.serializeAdvanced(serializationData, addLinebreak)
110
+ }
111
+
112
+ // Fallback to standard serialization
113
+ this.serializationStats.jsonSerializations++
114
+ return this.serializeStandard(serializationData, addLinebreak)
115
+ }
116
+
117
+
118
+
119
+ /**
120
+ * Advanced serialization with optimized JSON.stringify and buffer pooling
121
+ */
122
+ serializeAdvanced(data, addLinebreak) {
123
+ // CRITICAL FIX: Sanitize data to remove problematic characters before serialization
124
+ const sanitizedData = this.sanitizeDataForJSON(data)
125
+
126
+ // Validate encoding before serialization
127
+ this.validateEncodingBeforeSerialization(sanitizedData)
128
+
129
+ // Use optimized JSON.stringify without buffer pooling
130
+ // NOTE: Buffer pool removed - using direct Buffer creation for simplicity and reliability
131
+ const json = this.optimizedStringify(sanitizedData)
132
+
133
+ // CRITICAL FIX: Normalize encoding before creating buffer
134
+ const normalizedJson = this.normalizeEncoding(json)
135
+ const jsonBuffer = Buffer.from(normalizedJson, 'utf8')
136
+
137
+ const totalLength = jsonBuffer.length + (addLinebreak ? 1 : 0)
138
+ const result = Buffer.allocUnsafe(totalLength)
139
+
140
+ jsonBuffer.copy(result, 0, 0, jsonBuffer.length)
141
+ if (addLinebreak) {
142
+ result[jsonBuffer.length] = 0x0A
143
+ }
144
+
145
+ return result
146
+ }
147
+
148
+ /**
149
+ * Proper encoding normalization with UTF-8 validation
150
+ * Fixed to prevent double-encoding and data corruption
151
+ */
152
+ normalizeEncoding(str) {
153
+ if (typeof str !== 'string') return str
154
+
155
+ // Skip if already valid UTF-8 (99% of cases)
156
+ if (this.isValidUTF8(str)) return str
157
+
158
+ // Try to detect and convert encoding safely
159
+ return this.safeConvertToUTF8(str)
160
+ }
161
+
162
+ /**
163
+ * Check if string is valid UTF-8
164
+ */
165
+ isValidUTF8(str) {
166
+ try {
167
+ // Test if string can be encoded and decoded as UTF-8 without loss
168
+ const encoded = Buffer.from(str, 'utf8')
169
+ const decoded = encoded.toString('utf8')
170
+ return decoded === str
171
+ } catch (error) {
172
+ return false
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Safe conversion to UTF-8 with proper encoding detection
178
+ */
179
+ safeConvertToUTF8(str) {
180
+ // Try common encodings in order of likelihood
181
+ const encodings = ['utf8', 'latin1', 'utf16le', 'ascii']
182
+
183
+ for (const encoding of encodings) {
184
+ try {
185
+ const converted = Buffer.from(str, encoding).toString('utf8')
186
+
187
+ // Validate the conversion didn't lose information
188
+ if (this.isValidUTF8(converted)) {
189
+ return converted
190
+ }
191
+ } catch (error) {
192
+ // Try next encoding
193
+ continue
194
+ }
195
+ }
196
+
197
+ // Fallback: return original string (preserve data)
198
+ console.warn('JexiDB: Could not normalize encoding, preserving original string')
199
+ return str
200
+ }
201
+
202
+ /**
203
+ * Enhanced deep encoding normalization with UTF-8 validation
204
+ * Fixed to prevent double-encoding and data corruption
205
+ */
206
+ deepNormalizeEncoding(obj) {
207
+ if (obj === null || obj === undefined) return obj
208
+
209
+ if (typeof obj === 'string') {
210
+ return this.normalizeEncoding(obj)
211
+ }
212
+
213
+ if (Array.isArray(obj)) {
214
+ // Check if normalization is needed (performance optimization)
215
+ const needsNormalization = obj.some(item =>
216
+ typeof item === 'string' && !this.isValidUTF8(item)
217
+ )
218
+
219
+ if (!needsNormalization) return obj
220
+
221
+ return obj.map(item => this.deepNormalizeEncoding(item))
222
+ }
223
+
224
+ if (typeof obj === 'object') {
225
+ // Check if normalization is needed (performance optimization)
226
+ const needsNormalization = Object.values(obj).some(value =>
227
+ typeof value === 'string' && !this.isValidUTF8(value)
228
+ )
229
+
230
+ if (!needsNormalization) return obj
231
+
232
+ const normalized = {}
233
+ for (const [key, value] of Object.entries(obj)) {
234
+ normalized[key] = this.deepNormalizeEncoding(value)
235
+ }
236
+ return normalized
237
+ }
238
+
239
+ return obj
240
+ }
241
+
242
+ /**
243
+ * Validate encoding before serialization
244
+ */
245
+ /**
246
+ * Sanitize data to remove problematic characters that break JSON parsing
247
+ * CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors
248
+ * by removing control characters that cannot be safely represented in JSON
249
+ */
250
+ sanitizeDataForJSON(data) {
251
+ const sanitizeString = (str) => {
252
+ if (typeof str !== 'string') return str
253
+
254
+ return str
255
+ // Remove control characters that break JSON parsing (but keep \n, \r, \t as they can be escaped)
256
+ // Remove: NUL, SOH, STX, ETX, EOT, ENQ, ACK, BEL, VT, FF, SO, SI, DLE, DC1-DC4, NAK, SYN, ETB, CAN, EM, SUB, ESC, FS, GS, RS, US, DEL, C1 controls
257
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
258
+ // Limit string length to prevent performance issues
259
+ .substring(0, 10000)
260
+ }
261
+
262
+ const sanitizeArray = (arr) => {
263
+ if (!Array.isArray(arr)) return arr
264
+
265
+ return arr
266
+ .map(item => this.sanitizeDataForJSON(item))
267
+ .filter(item => item !== null && item !== undefined && item !== '')
268
+ }
269
+
270
+ if (typeof data === 'string') {
271
+ return sanitizeString(data)
272
+ }
273
+
274
+ if (Array.isArray(data)) {
275
+ return sanitizeArray(data)
276
+ }
277
+
278
+ if (data && typeof data === 'object') {
279
+ const sanitized = {}
280
+ for (const [key, value] of Object.entries(data)) {
281
+ const sanitizedValue = this.sanitizeDataForJSON(value)
282
+ // Only include non-null, non-undefined values
283
+ if (sanitizedValue !== null && sanitizedValue !== undefined) {
284
+ sanitized[key] = sanitizedValue
285
+ }
286
+ }
287
+ return sanitized
288
+ }
289
+
290
+ return data
291
+ }
292
+
293
+ validateEncodingBeforeSerialization(data) {
294
+ const issues = []
295
+
296
+ const checkString = (str, path = '') => {
297
+ if (typeof str === 'string' && !this.isValidUTF8(str)) {
298
+ issues.push(`Invalid encoding at ${path}: "${str.substring(0, 50)}..."`)
299
+ }
300
+ }
301
+
302
+ const traverse = (obj, path = '') => {
303
+ if (typeof obj === 'string') {
304
+ checkString(obj, path)
305
+ } else if (Array.isArray(obj)) {
306
+ obj.forEach((item, index) => {
307
+ traverse(item, `${path}[${index}]`)
308
+ })
309
+ } else if (obj && typeof obj === 'object') {
310
+ Object.entries(obj).forEach(([key, value]) => {
311
+ traverse(value, path ? `${path}.${key}` : key)
312
+ })
313
+ }
314
+ }
315
+
316
+ traverse(data)
317
+
318
+ if (issues.length > 0) {
319
+ console.warn('JexiDB: Encoding issues detected:', issues)
320
+ }
321
+
322
+ return issues.length === 0
323
+ }
324
+
325
+ /**
326
+ * Optimized JSON.stringify with fast paths for common data structures
327
+ * Now includes deep encoding normalization for all string fields
328
+ */
329
+ optimizedStringify(obj) {
330
+ // CRITICAL: Normalize encoding for all string fields before stringify
331
+ const normalizedObj = this.deepNormalizeEncoding(obj)
332
+ return this._stringifyNormalizedValue(normalizedObj)
333
+ }
334
+
335
+ _stringifyNormalizedValue(value) {
336
+ // Fast path for null and undefined
337
+ if (value === null || value === undefined) {
338
+ return 'null'
339
+ }
340
+
341
+ const type = typeof value
342
+
343
+ // Fast path for primitives
344
+ if (type === 'boolean') {
345
+ return value ? 'true' : 'false'
346
+ }
347
+
348
+ if (type === 'number') {
349
+ return Number.isFinite(value) ? value.toString() : 'null'
350
+ }
351
+
352
+ if (type === 'string') {
353
+ // Fast path for simple strings (no escaping needed)
354
+ if (!/[\\"\u0000-\u001f]/.test(value)) {
355
+ return '"' + value + '"'
356
+ }
357
+ // Fall back to JSON.stringify for complex strings
358
+ return JSON.stringify(value)
359
+ }
360
+
361
+ if (Array.isArray(value)) {
362
+ return this._stringifyNormalizedArray(value)
363
+ }
364
+
365
+ if (type === 'object') {
366
+ const keys = Object.keys(value)
367
+ if (keys.length === 0) return '{}'
368
+ // Use native stringify for object to leverage stable handling of undefined, Dates, etc.
369
+ return JSON.stringify(value)
370
+ }
371
+
372
+ // Fallback to JSON.stringify for unknown types (BigInt, symbols, etc.)
373
+ return JSON.stringify(value)
374
+ }
375
+
376
+ _stringifyNormalizedArray(arr) {
377
+ const length = arr.length
378
+ if (length === 0) return '[]'
379
+
380
+ let result = '['
381
+ for (let i = 0; i < length; i++) {
382
+ if (i > 0) result += ','
383
+ const element = arr[i]
384
+
385
+ // JSON spec: undefined, functions, and symbols are serialized as null within arrays
386
+ if (element === undefined || typeof element === 'function' || typeof element === 'symbol') {
387
+ result += 'null'
388
+ continue
389
+ }
390
+
391
+ result += this._stringifyNormalizedValue(element)
392
+ }
393
+ result += ']'
394
+ return result
395
+ }
396
+
397
+ /**
398
+ * Standard serialization (fallback)
399
+ */
400
+ serializeStandard(data, addLinebreak) {
401
+ // CRITICAL FIX: Sanitize data to remove problematic characters before serialization
402
+ const sanitizedData = this.sanitizeDataForJSON(data)
403
+
404
+ // Validate encoding before serialization
405
+ this.validateEncodingBeforeSerialization(sanitizedData)
406
+
407
+ // NOTE: Buffer pool removed - using direct Buffer creation for simplicity and reliability
408
+ // CRITICAL: Normalize encoding for all string fields before stringify
409
+ const normalizedData = this.deepNormalizeEncoding(sanitizedData)
410
+ const json = JSON.stringify(normalizedData)
411
+
412
+ // CRITICAL FIX: Normalize encoding before creating buffer
413
+ const normalizedJson = this.normalizeEncoding(json)
414
+ const jsonBuffer = Buffer.from(normalizedJson, 'utf8')
415
+
416
+ const totalLength = jsonBuffer.length + (addLinebreak ? 1 : 0)
417
+ const result = Buffer.allocUnsafe(totalLength)
418
+
419
+ jsonBuffer.copy(result, 0, 0, jsonBuffer.length)
420
+ if (addLinebreak) {
421
+ result[jsonBuffer.length] = 0x0A
422
+ }
423
+
424
+ return result
425
+ }
426
+
427
+ /**
428
+ * Advanced deserialization with fast paths
429
+ */
430
+ deserialize(data) {
431
+ this.serializationStats.totalDeserializations++
432
+
433
+ if (data.length === 0) return null
434
+
435
+ try {
436
+ // Handle both Buffer and string inputs
437
+ let str
438
+ if (Buffer.isBuffer(data)) {
439
+ // Fast path: avoid toString() for empty data
440
+ if (data.length === 1 && data[0] === 0x0A) return null // Just newline
441
+ str = data.toString('utf8').trim()
442
+ } else if (typeof data === 'string') {
443
+ str = data.trim()
444
+ } else {
445
+ throw new Error('Invalid data type for deserialization')
446
+ }
447
+
448
+ const strLength = str.length
449
+
450
+ // Fast path for empty strings
451
+ if (strLength === 0) return null
452
+
453
+ // CRITICAL FIX: Detect and handle multiple JSON objects in the same line
454
+ // This can happen if data was corrupted during concurrent writes or offset calculation errors
455
+ const firstBrace = str.indexOf('{')
456
+ const firstBracket = str.indexOf('[')
457
+
458
+ // Helper function to extract first complete JSON object/array from a string
459
+ // CRITICAL FIX: Must handle strings and escaped characters correctly
460
+ // to avoid counting braces/brackets that are inside string values
461
+ const extractFirstJson = (jsonStr, startChar) => {
462
+ if (startChar === '{') {
463
+ let braceCount = 0
464
+ let endPos = -1
465
+ let inString = false
466
+ let escapeNext = false
467
+
468
+ for (let i = 0; i < jsonStr.length; i++) {
469
+ const char = jsonStr[i]
470
+
471
+ if (escapeNext) {
472
+ escapeNext = false
473
+ continue
474
+ }
475
+
476
+ if (char === '\\') {
477
+ escapeNext = true
478
+ continue
479
+ }
480
+
481
+ if (char === '"' && !escapeNext) {
482
+ inString = !inString
483
+ continue
484
+ }
485
+
486
+ if (!inString) {
487
+ if (char === '{') braceCount++
488
+ if (char === '}') {
489
+ braceCount--
490
+ if (braceCount === 0) {
491
+ endPos = i + 1
492
+ break
493
+ }
494
+ }
495
+ }
496
+ }
497
+ return endPos > 0 ? jsonStr.substring(0, endPos) : null
498
+ } else if (startChar === '[') {
499
+ let bracketCount = 0
500
+ let endPos = -1
501
+ let inString = false
502
+ let escapeNext = false
503
+
504
+ for (let i = 0; i < jsonStr.length; i++) {
505
+ const char = jsonStr[i]
506
+
507
+ if (escapeNext) {
508
+ escapeNext = false
509
+ continue
510
+ }
511
+
512
+ if (char === '\\') {
513
+ escapeNext = true
514
+ continue
515
+ }
516
+
517
+ if (char === '"' && !escapeNext) {
518
+ inString = !inString
519
+ continue
520
+ }
521
+
522
+ if (!inString) {
523
+ if (char === '[') bracketCount++
524
+ if (char === ']') {
525
+ bracketCount--
526
+ if (bracketCount === 0) {
527
+ endPos = i + 1
528
+ break
529
+ }
530
+ }
531
+ }
532
+ }
533
+ return endPos > 0 ? jsonStr.substring(0, endPos) : null
534
+ }
535
+ return null
536
+ }
537
+
538
+ // Check if JSON starts at the beginning of the string
539
+ const jsonStartsAtZero = (firstBrace === 0) || (firstBracket === 0)
540
+ let hasValidJson = false
541
+
542
+ if (jsonStartsAtZero) {
543
+ // JSON starts at beginning - check for multiple JSON objects/arrays
544
+ if (firstBrace === 0) {
545
+ const secondBrace = str.indexOf('{', 1)
546
+ if (secondBrace !== -1) {
547
+ // Multiple objects detected - extract first
548
+ const extracted = extractFirstJson(str, '{')
549
+ if (extracted) {
550
+ str = extracted
551
+ hasValidJson = true
552
+ if (this.opts && this.opts.debugMode) {
553
+ console.warn(`⚠️ Deserialize: Multiple JSON objects detected, using first object only`)
554
+ }
555
+ }
556
+ } else {
557
+ hasValidJson = true // Single valid object starting at 0
558
+ }
559
+ } else if (firstBracket === 0) {
560
+ const secondBracket = str.indexOf('[', 1)
561
+ if (secondBracket !== -1) {
562
+ // Multiple arrays detected - extract first
563
+ const extracted = extractFirstJson(str, '[')
564
+ if (extracted) {
565
+ str = extracted
566
+ hasValidJson = true
567
+ if (this.opts && this.opts.debugMode) {
568
+ console.warn(`⚠️ Deserialize: Multiple JSON arrays detected, using first array only`)
569
+ }
570
+ }
571
+ } else {
572
+ hasValidJson = true // Single valid array starting at 0
573
+ }
574
+ }
575
+ } else {
576
+ // JSON doesn't start at beginning - try to find and extract first valid JSON
577
+ const jsonStart = firstBrace !== -1 ? (firstBracket !== -1 ? Math.min(firstBrace, firstBracket) : firstBrace) : firstBracket
578
+
579
+ if (jsonStart !== -1 && jsonStart > 0) {
580
+ // Found JSON but not at start - extract from that position
581
+ const jsonStr = str.substring(jsonStart)
582
+ const startChar = jsonStr[0]
583
+ const extracted = extractFirstJson(jsonStr, startChar)
584
+
585
+ if (extracted) {
586
+ str = extracted
587
+ hasValidJson = true
588
+ if (this.opts && this.opts.debugMode) {
589
+ console.warn(`⚠️ Deserialize: Found JSON after ${jsonStart} chars of invalid text, extracted first ${startChar === '{' ? 'object' : 'array'}`)
590
+ }
591
+ }
592
+ }
593
+ }
594
+
595
+ // CRITICAL FIX: If no valid JSON structure found, throw error before attempting parse
596
+ // This allows walk() and other callers to catch and skip invalid lines
597
+ if (!hasValidJson && firstBrace === -1 && firstBracket === -1) {
598
+ const errorStr = Buffer.isBuffer(data) ? data.toString('utf8').trim() : data.trim()
599
+ const error = new Error(`Failed to deserialize JSON data: No valid JSON structure found in "${errorStr.substring(0, 100)}..."`)
600
+ // Mark this as a "no valid JSON" error so it can be handled appropriately
601
+ error.noValidJson = true
602
+ throw error
603
+ }
604
+
605
+ // If we tried to extract but got nothing valid, also throw error
606
+ if (hasValidJson && (!str || str.trim().length === 0)) {
607
+ const error = new Error(`Failed to deserialize JSON data: Extracted JSON is empty`)
608
+ error.noValidJson = true
609
+ throw error
610
+ }
611
+
612
+ // Parse JSON data
613
+ const parsedData = JSON.parse(str)
614
+
615
+ // Convert from array format back to object if needed
616
+ return this.convertFromArrayFormat(parsedData)
617
+ } catch (e) {
618
+ // If error was already formatted with noValidJson flag, re-throw as-is
619
+ if (e.noValidJson) {
620
+ throw e
621
+ }
622
+ // Otherwise, format the error message
623
+ const str = Buffer.isBuffer(data) ? data.toString('utf8').trim() : data.trim()
624
+ throw new Error(`Failed to deserialize JSON data: "${str.substring(0, 100)}..." - ${e.message}`)
625
+ }
626
+ }
627
+
628
+ /**
629
+ * Batch serialization for multiple records
630
+ */
631
+ serializeBatch(dataArray, opts = {}) {
632
+ // CRITICAL FIX: Sanitize data to remove problematic characters before serialization
633
+ const sanitizedDataArray = dataArray.map(data => this.sanitizeDataForJSON(data))
634
+
635
+ // Validate encoding before serialization
636
+ this.validateEncodingBeforeSerialization(sanitizedDataArray)
637
+
638
+ // Convert all objects to array format for optimization
639
+ const convertedData = sanitizedDataArray.map(data => this.convertToArrayFormat(data))
640
+
641
+ // Track conversion statistics
642
+ this.serializationStats.arraySerializations += convertedData.filter((item, index) =>
643
+ Array.isArray(item) && typeof dataArray[index] === 'object' && dataArray[index] !== null
644
+ ).length
645
+ this.serializationStats.objectSerializations += dataArray.length - this.serializationStats.arraySerializations
646
+
647
+ // JSONL format: serialize each array as a separate line
648
+ try {
649
+ const lines = []
650
+ for (const arrayData of convertedData) {
651
+ const json = this.optimizedStringify(arrayData)
652
+ const normalizedJson = this.normalizeEncoding(json)
653
+ lines.push(normalizedJson)
654
+ }
655
+
656
+ // Join all lines with newlines
657
+ const jsonlContent = lines.join('\n')
658
+ const jsonlBuffer = Buffer.from(jsonlContent, 'utf8')
659
+
660
+ // Add final linebreak if requested
661
+ const addLinebreak = opts.linebreak !== false
662
+ const totalLength = jsonlBuffer.length + (addLinebreak ? 1 : 0)
663
+ const result = Buffer.allocUnsafe(totalLength)
664
+
665
+ jsonlBuffer.copy(result, 0, 0, jsonlBuffer.length)
666
+ if (addLinebreak) {
667
+ result[jsonlBuffer.length] = 0x0A
668
+ }
669
+
670
+ return result
671
+ } catch (e) {
672
+ // Fallback to individual serialization if batch serialization fails
673
+ const results = []
674
+ const batchSize = opts.batchSize || 100
675
+
676
+ for (let i = 0; i < convertedData.length; i += batchSize) {
677
+ const batch = convertedData.slice(i, i + batchSize)
678
+ const batchResults = batch.map(data => this.serialize(data, opts))
679
+ results.push(...batchResults)
680
+ }
681
+
682
+ return results
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Batch deserialization for multiple records
688
+ */
689
+ deserializeBatch(dataArray) {
690
+ // Optimization: try to parse all entries as a single JSON array first
691
+ // This is much faster than parsing each entry individually
692
+ try {
693
+ // Convert all entries to strings and join them as a single JSON array
694
+ const entriesJson = '[' + dataArray.map(data => {
695
+ if (Buffer.isBuffer(data)) {
696
+ return data.toString('utf8').trim()
697
+ } else if (typeof data === 'string') {
698
+ return data.trim()
699
+ } else {
700
+ throw new Error('Invalid data type for batch deserialization')
701
+ }
702
+ }).join(',') + ']'
703
+ const parsedResults = JSON.parse(entriesJson)
704
+
705
+ // Convert arrays back to objects if needed
706
+ const results = parsedResults.map(data => this.convertFromArrayFormat(data))
707
+
708
+ // Validate that all results are objects (JexiDB requirement)
709
+ if (Array.isArray(results) && results.every(item => item && typeof item === 'object')) {
710
+ return results
711
+ }
712
+
713
+ // If validation fails, fall back to individual parsing
714
+ throw new Error('Validation failed - not all entries are objects')
715
+ } catch (e) {
716
+ // Fallback to individual deserialization if batch parsing fails
717
+ const results = []
718
+ const batchSize = 100 // Process in batches to avoid blocking
719
+
720
+ for (let i = 0; i < dataArray.length; i += batchSize) {
721
+ const batch = dataArray.slice(i, i + batchSize)
722
+ const batchResults = batch.map(data => this.deserialize(data))
723
+ results.push(...batchResults)
724
+ }
725
+
726
+ return results
727
+ }
728
+ }
729
+
730
+ /**
731
+ * Check if data appears to be binary (always false since we only use JSON now)
732
+ */
733
+ isBinaryData(data) {
734
+ // All data is now JSON format
735
+ return false
736
+ }
737
+
738
+ /**
739
+ * Get comprehensive performance statistics
740
+ */
741
+ getStats() {
742
+ // NOTE: Buffer pool stats removed - buffer pool was causing more problems than benefits
743
+ return {
744
+ // Serialization stats
745
+ totalSerializations: this.serializationStats.totalSerializations,
746
+ totalDeserializations: this.serializationStats.totalDeserializations,
747
+ jsonSerializations: this.serializationStats.jsonSerializations,
748
+ arraySerializations: this.serializationStats.arraySerializations,
749
+ objectSerializations: this.serializationStats.objectSerializations,
750
+
751
+ // Configuration
752
+ enableAdvancedSerialization: this.opts.enableAdvancedSerialization,
753
+ enableArraySerialization: this.opts.enableArraySerialization,
754
+
755
+ // Schema stats
756
+ schemaStats: this.schemaManager.getStats()
757
+ }
758
+ }
759
+
760
+ /**
761
+ * Cleanup resources
762
+ */
763
+ cleanup() {
764
+ // NOTE: Buffer pool cleanup removed - buffer pool was causing more problems than benefits
765
+ this.serializationStats = {
766
+ totalSerializations: 0,
767
+ totalDeserializations: 0,
768
+ jsonSerializations: 0,
769
+ arraySerializations: 0,
770
+ objectSerializations: 0
771
+ }
772
+
773
+ // Reset schema manager
774
+ if (this.schemaManager) {
775
+ this.schemaManager.reset()
776
+ }
777
+ }
778
778
  }