jexidb 2.1.0 → 2.1.2
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.
- package/dist/Database.cjs +9253 -437
- package/package.json +9 -2
- package/src/Database.mjs +1572 -212
- package/src/FileHandler.mjs +83 -44
- package/src/OperationQueue.mjs +23 -23
- package/src/SchemaManager.mjs +325 -268
- package/src/Serializer.mjs +234 -24
- package/src/managers/IndexManager.mjs +778 -87
- package/src/managers/QueryManager.mjs +340 -67
- package/src/managers/TermManager.mjs +7 -7
- package/src/utils/operatorNormalizer.mjs +116 -0
- package/.babelrc +0 -13
- package/.gitattributes +0 -2
- package/CHANGELOG.md +0 -140
- package/babel.config.json +0 -5
- package/docs/API.md +0 -1051
- package/docs/EXAMPLES.md +0 -701
- package/docs/README.md +0 -194
- package/examples/iterate-usage-example.js +0 -157
- package/examples/simple-iterate-example.js +0 -115
- package/jest.config.js +0 -24
- package/scripts/README.md +0 -47
- package/scripts/clean-test-files.js +0 -75
- package/scripts/prepare.js +0 -31
- package/scripts/run-tests.js +0 -80
- package/test/$not-operator-with-and.test.js +0 -282
- package/test/README.md +0 -8
- package/test/close-init-cycle.test.js +0 -256
- package/test/critical-bugs-fixes.test.js +0 -1069
- package/test/index-persistence.test.js +0 -306
- package/test/index-serialization.test.js +0 -314
- package/test/indexed-query-mode.test.js +0 -360
- package/test/iterate-method.test.js +0 -272
- package/test/query-operators.test.js +0 -238
- package/test/regex-array-fields.test.js +0 -129
- package/test/score-method.test.js +0 -238
- package/test/setup.js +0 -17
- package/test/term-mapping-minimal.test.js +0 -154
- package/test/term-mapping-simple.test.js +0 -257
- package/test/term-mapping.test.js +0 -514
- package/test/writebuffer-flush-resilience.test.js +0 -204
package/src/Serializer.mjs
CHANGED
|
@@ -64,7 +64,26 @@ export default class Serializer {
|
|
|
64
64
|
if (!this.opts.enableArraySerialization) {
|
|
65
65
|
return arr
|
|
66
66
|
}
|
|
67
|
-
|
|
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
|
|
68
87
|
}
|
|
69
88
|
|
|
70
89
|
/**
|
|
@@ -259,42 +278,69 @@ export default class Serializer {
|
|
|
259
278
|
optimizedStringify(obj) {
|
|
260
279
|
// CRITICAL: Normalize encoding for all string fields before stringify
|
|
261
280
|
const normalizedObj = this.deepNormalizeEncoding(obj)
|
|
262
|
-
|
|
281
|
+
return this._stringifyNormalizedValue(normalizedObj)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_stringifyNormalizedValue(value) {
|
|
263
285
|
// Fast path for null and undefined
|
|
264
|
-
if (
|
|
265
|
-
|
|
286
|
+
if (value === null || value === undefined) {
|
|
287
|
+
return 'null'
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const type = typeof value
|
|
266
291
|
|
|
267
292
|
// Fast path for primitives
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
293
|
+
if (type === 'boolean') {
|
|
294
|
+
return value ? 'true' : 'false'
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (type === 'number') {
|
|
298
|
+
return Number.isFinite(value) ? value.toString() : 'null'
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (type === 'string') {
|
|
271
302
|
// Fast path for simple strings (no escaping needed)
|
|
272
|
-
if (!/[\\"\u0000-\u001f]/.test(
|
|
273
|
-
return '"' +
|
|
303
|
+
if (!/[\\"\u0000-\u001f]/.test(value)) {
|
|
304
|
+
return '"' + value + '"'
|
|
274
305
|
}
|
|
275
306
|
// Fall back to JSON.stringify for complex strings
|
|
276
|
-
return JSON.stringify(
|
|
307
|
+
return JSON.stringify(value)
|
|
277
308
|
}
|
|
278
309
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (normalizedObj.length === 0) return '[]'
|
|
282
|
-
|
|
283
|
-
// For arrays, always use JSON.stringify to avoid concatenation issues
|
|
284
|
-
return JSON.stringify(normalizedObj)
|
|
310
|
+
if (Array.isArray(value)) {
|
|
311
|
+
return this._stringifyNormalizedArray(value)
|
|
285
312
|
}
|
|
286
313
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const keys = Object.keys(normalizedObj)
|
|
314
|
+
if (type === 'object') {
|
|
315
|
+
const keys = Object.keys(value)
|
|
290
316
|
if (keys.length === 0) return '{}'
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return JSON.stringify(normalizedObj)
|
|
317
|
+
// Use native stringify for object to leverage stable handling of undefined, Dates, etc.
|
|
318
|
+
return JSON.stringify(value)
|
|
294
319
|
}
|
|
295
320
|
|
|
296
|
-
// Fallback to JSON.stringify for unknown types
|
|
297
|
-
return JSON.stringify(
|
|
321
|
+
// Fallback to JSON.stringify for unknown types (BigInt, symbols, etc.)
|
|
322
|
+
return JSON.stringify(value)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
_stringifyNormalizedArray(arr) {
|
|
326
|
+
const length = arr.length
|
|
327
|
+
if (length === 0) return '[]'
|
|
328
|
+
|
|
329
|
+
let result = '['
|
|
330
|
+
for (let i = 0; i < length; i++) {
|
|
331
|
+
if (i > 0) result += ','
|
|
332
|
+
const element = arr[i]
|
|
333
|
+
|
|
334
|
+
// JSON spec: undefined, functions, and symbols are serialized as null within arrays
|
|
335
|
+
if (element === undefined || typeof element === 'function' || typeof element === 'symbol') {
|
|
336
|
+
result += 'null'
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
result += this._stringifyNormalizedValue(element)
|
|
341
|
+
}
|
|
342
|
+
result += ']'
|
|
343
|
+
return result
|
|
298
344
|
}
|
|
299
345
|
|
|
300
346
|
/**
|
|
@@ -350,12 +396,176 @@ export default class Serializer {
|
|
|
350
396
|
// Fast path for empty strings
|
|
351
397
|
if (strLength === 0) return null
|
|
352
398
|
|
|
399
|
+
// CRITICAL FIX: Detect and handle multiple JSON objects in the same line
|
|
400
|
+
// This can happen if data was corrupted during concurrent writes or offset calculation errors
|
|
401
|
+
const firstBrace = str.indexOf('{')
|
|
402
|
+
const firstBracket = str.indexOf('[')
|
|
403
|
+
|
|
404
|
+
// Helper function to extract first complete JSON object/array from a string
|
|
405
|
+
// CRITICAL FIX: Must handle strings and escaped characters correctly
|
|
406
|
+
// to avoid counting braces/brackets that are inside string values
|
|
407
|
+
const extractFirstJson = (jsonStr, startChar) => {
|
|
408
|
+
if (startChar === '{') {
|
|
409
|
+
let braceCount = 0
|
|
410
|
+
let endPos = -1
|
|
411
|
+
let inString = false
|
|
412
|
+
let escapeNext = false
|
|
413
|
+
|
|
414
|
+
for (let i = 0; i < jsonStr.length; i++) {
|
|
415
|
+
const char = jsonStr[i]
|
|
416
|
+
|
|
417
|
+
if (escapeNext) {
|
|
418
|
+
escapeNext = false
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (char === '\\') {
|
|
423
|
+
escapeNext = true
|
|
424
|
+
continue
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (char === '"' && !escapeNext) {
|
|
428
|
+
inString = !inString
|
|
429
|
+
continue
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!inString) {
|
|
433
|
+
if (char === '{') braceCount++
|
|
434
|
+
if (char === '}') {
|
|
435
|
+
braceCount--
|
|
436
|
+
if (braceCount === 0) {
|
|
437
|
+
endPos = i + 1
|
|
438
|
+
break
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return endPos > 0 ? jsonStr.substring(0, endPos) : null
|
|
444
|
+
} else if (startChar === '[') {
|
|
445
|
+
let bracketCount = 0
|
|
446
|
+
let endPos = -1
|
|
447
|
+
let inString = false
|
|
448
|
+
let escapeNext = false
|
|
449
|
+
|
|
450
|
+
for (let i = 0; i < jsonStr.length; i++) {
|
|
451
|
+
const char = jsonStr[i]
|
|
452
|
+
|
|
453
|
+
if (escapeNext) {
|
|
454
|
+
escapeNext = false
|
|
455
|
+
continue
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (char === '\\') {
|
|
459
|
+
escapeNext = true
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (char === '"' && !escapeNext) {
|
|
464
|
+
inString = !inString
|
|
465
|
+
continue
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!inString) {
|
|
469
|
+
if (char === '[') bracketCount++
|
|
470
|
+
if (char === ']') {
|
|
471
|
+
bracketCount--
|
|
472
|
+
if (bracketCount === 0) {
|
|
473
|
+
endPos = i + 1
|
|
474
|
+
break
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return endPos > 0 ? jsonStr.substring(0, endPos) : null
|
|
480
|
+
}
|
|
481
|
+
return null
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Check if JSON starts at the beginning of the string
|
|
485
|
+
const jsonStartsAtZero = (firstBrace === 0) || (firstBracket === 0)
|
|
486
|
+
let hasValidJson = false
|
|
487
|
+
|
|
488
|
+
if (jsonStartsAtZero) {
|
|
489
|
+
// JSON starts at beginning - check for multiple JSON objects/arrays
|
|
490
|
+
if (firstBrace === 0) {
|
|
491
|
+
const secondBrace = str.indexOf('{', 1)
|
|
492
|
+
if (secondBrace !== -1) {
|
|
493
|
+
// Multiple objects detected - extract first
|
|
494
|
+
const extracted = extractFirstJson(str, '{')
|
|
495
|
+
if (extracted) {
|
|
496
|
+
str = extracted
|
|
497
|
+
hasValidJson = true
|
|
498
|
+
if (this.opts && this.opts.debugMode) {
|
|
499
|
+
console.warn(`⚠️ Deserialize: Multiple JSON objects detected, using first object only`)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
hasValidJson = true // Single valid object starting at 0
|
|
504
|
+
}
|
|
505
|
+
} else if (firstBracket === 0) {
|
|
506
|
+
const secondBracket = str.indexOf('[', 1)
|
|
507
|
+
if (secondBracket !== -1) {
|
|
508
|
+
// Multiple arrays detected - extract first
|
|
509
|
+
const extracted = extractFirstJson(str, '[')
|
|
510
|
+
if (extracted) {
|
|
511
|
+
str = extracted
|
|
512
|
+
hasValidJson = true
|
|
513
|
+
if (this.opts && this.opts.debugMode) {
|
|
514
|
+
console.warn(`⚠️ Deserialize: Multiple JSON arrays detected, using first array only`)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
hasValidJson = true // Single valid array starting at 0
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
// JSON doesn't start at beginning - try to find and extract first valid JSON
|
|
523
|
+
const jsonStart = firstBrace !== -1 ? (firstBracket !== -1 ? Math.min(firstBrace, firstBracket) : firstBrace) : firstBracket
|
|
524
|
+
|
|
525
|
+
if (jsonStart !== -1 && jsonStart > 0) {
|
|
526
|
+
// Found JSON but not at start - extract from that position
|
|
527
|
+
const jsonStr = str.substring(jsonStart)
|
|
528
|
+
const startChar = jsonStr[0]
|
|
529
|
+
const extracted = extractFirstJson(jsonStr, startChar)
|
|
530
|
+
|
|
531
|
+
if (extracted) {
|
|
532
|
+
str = extracted
|
|
533
|
+
hasValidJson = true
|
|
534
|
+
if (this.opts && this.opts.debugMode) {
|
|
535
|
+
console.warn(`⚠️ Deserialize: Found JSON after ${jsonStart} chars of invalid text, extracted first ${startChar === '{' ? 'object' : 'array'}`)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// CRITICAL FIX: If no valid JSON structure found, throw error before attempting parse
|
|
542
|
+
// This allows walk() and other callers to catch and skip invalid lines
|
|
543
|
+
if (!hasValidJson && firstBrace === -1 && firstBracket === -1) {
|
|
544
|
+
const errorStr = Buffer.isBuffer(data) ? data.toString('utf8').trim() : data.trim()
|
|
545
|
+
const error = new Error(`Failed to deserialize JSON data: No valid JSON structure found in "${errorStr.substring(0, 100)}..."`)
|
|
546
|
+
// Mark this as a "no valid JSON" error so it can be handled appropriately
|
|
547
|
+
error.noValidJson = true
|
|
548
|
+
throw error
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// If we tried to extract but got nothing valid, also throw error
|
|
552
|
+
if (hasValidJson && (!str || str.trim().length === 0)) {
|
|
553
|
+
const error = new Error(`Failed to deserialize JSON data: Extracted JSON is empty`)
|
|
554
|
+
error.noValidJson = true
|
|
555
|
+
throw error
|
|
556
|
+
}
|
|
557
|
+
|
|
353
558
|
// Parse JSON data
|
|
354
559
|
const parsedData = JSON.parse(str)
|
|
355
560
|
|
|
356
561
|
// Convert from array format back to object if needed
|
|
357
562
|
return this.convertFromArrayFormat(parsedData)
|
|
358
563
|
} catch (e) {
|
|
564
|
+
// If error was already formatted with noValidJson flag, re-throw as-is
|
|
565
|
+
if (e.noValidJson) {
|
|
566
|
+
throw e
|
|
567
|
+
}
|
|
568
|
+
// Otherwise, format the error message
|
|
359
569
|
const str = Buffer.isBuffer(data) ? data.toString('utf8').trim() : data.trim()
|
|
360
570
|
throw new Error(`Failed to deserialize JSON data: "${str.substring(0, 100)}..." - ${e.message}`)
|
|
361
571
|
}
|