jexidb 2.0.3 → 2.1.1

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.
Files changed (79) hide show
  1. package/.babelrc +13 -0
  2. package/.gitattributes +2 -0
  3. package/CHANGELOG.md +132 -101
  4. package/LICENSE +21 -21
  5. package/README.md +301 -639
  6. package/babel.config.json +5 -0
  7. package/dist/Database.cjs +5204 -0
  8. package/docs/API.md +908 -241
  9. package/docs/EXAMPLES.md +701 -177
  10. package/docs/README.md +194 -184
  11. package/examples/iterate-usage-example.js +157 -0
  12. package/examples/simple-iterate-example.js +115 -0
  13. package/jest.config.js +24 -0
  14. package/package.json +63 -54
  15. package/scripts/README.md +47 -0
  16. package/scripts/benchmark-array-serialization.js +108 -0
  17. package/scripts/clean-test-files.js +75 -0
  18. package/scripts/prepare.js +31 -0
  19. package/scripts/run-tests.js +80 -0
  20. package/scripts/score-mode-demo.js +45 -0
  21. package/src/Database.mjs +5325 -0
  22. package/src/FileHandler.mjs +1140 -0
  23. package/src/OperationQueue.mjs +279 -0
  24. package/src/SchemaManager.mjs +268 -0
  25. package/src/Serializer.mjs +702 -0
  26. package/src/managers/ConcurrencyManager.mjs +257 -0
  27. package/src/managers/IndexManager.mjs +2094 -0
  28. package/src/managers/QueryManager.mjs +1490 -0
  29. package/src/managers/StatisticsManager.mjs +262 -0
  30. package/src/managers/StreamingProcessor.mjs +429 -0
  31. package/src/managers/TermManager.mjs +278 -0
  32. package/src/utils/operatorNormalizer.mjs +116 -0
  33. package/test/$not-operator-with-and.test.js +282 -0
  34. package/test/README.md +8 -0
  35. package/test/close-init-cycle.test.js +256 -0
  36. package/test/coverage-method.test.js +93 -0
  37. package/test/critical-bugs-fixes.test.js +1069 -0
  38. package/test/deserialize-corruption-fixes.test.js +296 -0
  39. package/test/exists-method.test.js +318 -0
  40. package/test/explicit-indexes-comparison.test.js +219 -0
  41. package/test/filehandler-non-adjacent-ranges-bug.test.js +175 -0
  42. package/test/index-line-number-regression.test.js +100 -0
  43. package/test/index-missing-index-data.test.js +91 -0
  44. package/test/index-persistence.test.js +491 -0
  45. package/test/index-serialization.test.js +314 -0
  46. package/test/indexed-query-mode.test.js +360 -0
  47. package/test/insert-session-auto-flush.test.js +353 -0
  48. package/test/iterate-method.test.js +272 -0
  49. package/test/legacy-operator-compat.test.js +154 -0
  50. package/test/query-operators.test.js +238 -0
  51. package/test/regex-array-fields.test.js +129 -0
  52. package/test/score-method.test.js +298 -0
  53. package/test/setup.js +17 -0
  54. package/test/term-mapping-minimal.test.js +154 -0
  55. package/test/term-mapping-simple.test.js +257 -0
  56. package/test/term-mapping.test.js +514 -0
  57. package/test/writebuffer-flush-resilience.test.js +204 -0
  58. package/dist/FileHandler.js +0 -688
  59. package/dist/IndexManager.js +0 -353
  60. package/dist/IntegrityChecker.js +0 -364
  61. package/dist/JSONLDatabase.js +0 -1333
  62. package/dist/index.js +0 -617
  63. package/docs/MIGRATION.md +0 -295
  64. package/examples/auto-save-example.js +0 -158
  65. package/examples/cjs-usage.cjs +0 -82
  66. package/examples/close-vs-delete-example.js +0 -71
  67. package/examples/esm-usage.js +0 -113
  68. package/examples/example-columns.idx.jdb +0 -0
  69. package/examples/example-columns.jdb +0 -9
  70. package/examples/example-options.idx.jdb +0 -0
  71. package/examples/example-options.jdb +0 -0
  72. package/examples/example-users.idx.jdb +0 -0
  73. package/examples/example-users.jdb +0 -5
  74. package/examples/simple-test.js +0 -55
  75. package/src/FileHandler.js +0 -674
  76. package/src/IndexManager.js +0 -363
  77. package/src/IntegrityChecker.js +0 -379
  78. package/src/JSONLDatabase.js +0 -1391
  79. package/src/index.js +0 -608
@@ -0,0 +1,279 @@
1
+ /**
2
+ * OperationQueue - Queue system for database operations
3
+ * Resolves race conditions between concurrent operations
4
+ */
5
+
6
+ export class OperationQueue {
7
+ constructor(debugMode = false) {
8
+ this.queue = []
9
+ this.processing = false
10
+ this.operationId = 0
11
+ this.debugMode = debugMode
12
+ this.stats = {
13
+ totalOperations: 0,
14
+ completedOperations: 0,
15
+ failedOperations: 0,
16
+ averageProcessingTime: 0,
17
+ maxProcessingTime: 0,
18
+ totalProcessingTime: 0
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Adds an operation to the queue
24
+ * @param {Function} operation - Asynchronous function to be executed
25
+ * @returns {Promise} - Promise that resolves when the operation is completed
26
+ */
27
+ async enqueue(operation) {
28
+ const id = ++this.operationId
29
+ const startTime = Date.now()
30
+
31
+ if (this.debugMode) {
32
+ console.log(`🔄 Queue: Enqueuing operation ${id}, queue length: ${this.queue.length}`)
33
+ }
34
+
35
+ this.stats.totalOperations++
36
+
37
+ return new Promise((resolve, reject) => {
38
+ // Capture stack trace for debugging stuck operations
39
+ const stackTrace = new Error().stack
40
+
41
+ this.queue.push({
42
+ id,
43
+ operation,
44
+ resolve,
45
+ reject,
46
+ timestamp: startTime,
47
+ stackTrace: stackTrace,
48
+ startTime: Date.now()
49
+ })
50
+
51
+ // Process immediately if not already processing
52
+ this.process().catch(reject)
53
+ })
54
+ }
55
+
56
+ /**
57
+ * Processes all operations in the queue sequentially
58
+ */
59
+ async process() {
60
+ if (this.processing || this.queue.length === 0) {
61
+ return
62
+ }
63
+
64
+ this.processing = true
65
+
66
+ if (this.debugMode) {
67
+ console.log(`🔄 Queue: Starting to process ${this.queue.length} operations`)
68
+ }
69
+
70
+ try {
71
+ while (this.queue.length > 0) {
72
+ const { id, operation, resolve, reject, timestamp } = this.queue.shift()
73
+
74
+ if (this.debugMode) {
75
+ console.log(`🔄 Queue: Processing operation ${id}`)
76
+ }
77
+
78
+ try {
79
+ const result = await operation()
80
+ const processingTime = Date.now() - timestamp
81
+
82
+ // Atualizar estatísticas
83
+ this.stats.completedOperations++
84
+ this.stats.totalProcessingTime += processingTime
85
+ this.stats.averageProcessingTime = this.stats.totalProcessingTime / this.stats.completedOperations
86
+ this.stats.maxProcessingTime = Math.max(this.stats.maxProcessingTime, processingTime)
87
+
88
+ resolve(result)
89
+
90
+ if (this.debugMode) {
91
+ console.log(`✅ Queue: Operation ${id} completed in ${processingTime}ms`)
92
+ }
93
+ } catch (error) {
94
+ const processingTime = Date.now() - timestamp
95
+
96
+ // Atualizar estatísticas
97
+ this.stats.failedOperations++
98
+ this.stats.totalProcessingTime += processingTime
99
+ this.stats.averageProcessingTime = this.stats.totalProcessingTime / (this.stats.completedOperations + this.stats.failedOperations)
100
+ this.stats.maxProcessingTime = Math.max(this.stats.maxProcessingTime, processingTime)
101
+
102
+ reject(error)
103
+
104
+ if (this.debugMode) {
105
+ console.error(`❌ Queue: Operation ${id} failed in ${processingTime}ms:`, error)
106
+ }
107
+ }
108
+ }
109
+ } finally {
110
+ this.processing = false
111
+
112
+ if (this.debugMode) {
113
+ console.log(`🔄 Queue: Finished processing, remaining: ${this.queue.length}`)
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Waits for all pending operations to be processed
120
+ * @param {number|null} maxWaitTime - Maximum wait time in ms (null = wait indefinitely)
121
+ * @returns {Promise<boolean>} - true if all operations were processed, false if a timeout occurred
122
+ */
123
+ async waitForCompletion(maxWaitTime = 5000) {
124
+ const startTime = Date.now()
125
+
126
+ // CRITICAL FIX: Support infinite wait when maxWaitTime is null
127
+ const hasTimeout = maxWaitTime !== null && maxWaitTime !== undefined
128
+
129
+ while (this.queue.length > 0) {
130
+ // Check timeout only if we have one
131
+ if (hasTimeout && (Date.now() - startTime) >= maxWaitTime) {
132
+ break
133
+ }
134
+
135
+ await new Promise(resolve => setTimeout(resolve, 1))
136
+ }
137
+
138
+ const completed = this.queue.length === 0
139
+ if (!completed && hasTimeout) {
140
+ // CRITICAL: Don't leave operations hanging - fail fast with detailed error
141
+ const pendingOperations = this.queue.map(op => ({
142
+ id: op.id,
143
+ stackTrace: op.stackTrace,
144
+ startTime: op.startTime,
145
+ waitTime: Date.now() - op.startTime
146
+ }))
147
+
148
+ // Clear the queue to prevent memory leaks
149
+ this.queue = []
150
+
151
+ const error = new Error(`OperationQueue: Operations timed out after ${maxWaitTime}ms. ${pendingOperations.length} operations were stuck and have been cleared.`)
152
+ error.pendingOperations = pendingOperations
153
+ error.queueStats = this.getStats()
154
+
155
+ if (this.debugMode) {
156
+ console.error(`❌ Queue: Operations timed out, clearing ${pendingOperations.length} stuck operations:`)
157
+ pendingOperations.forEach(op => {
158
+ console.error(` - Operation ${op.id} (waiting ${op.waitTime}ms):`)
159
+ console.error(` Stack: ${op.stackTrace}`)
160
+ })
161
+ }
162
+
163
+ throw error
164
+ }
165
+
166
+ return completed
167
+ }
168
+
169
+ /**
170
+ * Returns the current queue length
171
+ */
172
+ getQueueLength() {
173
+ return this.queue.length
174
+ }
175
+
176
+ /**
177
+ * Checks whether operations are currently being processed
178
+ */
179
+ isProcessing() {
180
+ return this.processing
181
+ }
182
+
183
+ /**
184
+ * Returns queue statistics
185
+ */
186
+ getStats() {
187
+ return {
188
+ ...this.stats,
189
+ queueLength: this.queue.length,
190
+ isProcessing: this.processing,
191
+ successRate: this.stats.totalOperations > 0 ?
192
+ (this.stats.completedOperations / this.stats.totalOperations) * 100 : 0
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Clears the queue (for emergency situations)
198
+ */
199
+ clear() {
200
+ const clearedCount = this.queue.length
201
+ this.queue = []
202
+
203
+ if (this.debugMode) {
204
+ console.log(`🧹 Queue: Cleared ${clearedCount} pending operations`)
205
+ }
206
+
207
+ return clearedCount
208
+ }
209
+
210
+ /**
211
+ * Detects stuck operations and returns detailed information
212
+ * @param {number} stuckThreshold - Time in ms to consider an operation stuck
213
+ * @returns {Array} - List of stuck operations with stack traces
214
+ */
215
+ detectStuckOperations(stuckThreshold = 10000) {
216
+ const now = Date.now()
217
+ const stuckOperations = this.queue.filter(op => (now - op.startTime) > stuckThreshold)
218
+
219
+ return stuckOperations.map(op => ({
220
+ id: op.id,
221
+ waitTime: now - op.startTime,
222
+ stackTrace: op.stackTrace,
223
+ timestamp: op.timestamp
224
+ }))
225
+ }
226
+
227
+ /**
228
+ * Force-cleans stuck operations (last resort)
229
+ * @param {number} stuckThreshold - Time in ms to consider an operation stuck
230
+ * @returns {number} - Number of operations removed
231
+ */
232
+ forceCleanupStuckOperations(stuckThreshold = 10000) {
233
+ const stuckOps = this.detectStuckOperations(stuckThreshold)
234
+
235
+ if (stuckOps.length > 0) {
236
+ // Reject all stuck operations
237
+ stuckOps.forEach(stuckOp => {
238
+ const opIndex = this.queue.findIndex(op => op.id === stuckOp.id)
239
+ if (opIndex !== -1) {
240
+ const op = this.queue[opIndex]
241
+ op.reject(new Error(`Operation ${op.id} was stuck for ${stuckOp.waitTime}ms and has been force-cleaned. Stack: ${stuckOp.stackTrace}`))
242
+ this.queue.splice(opIndex, 1)
243
+ }
244
+ })
245
+
246
+ if (this.debugMode) {
247
+ console.error(`🧹 Queue: Force-cleaned ${stuckOps.length} stuck operations`)
248
+ stuckOps.forEach(op => {
249
+ console.error(` - Operation ${op.id} (stuck for ${op.waitTime}ms)`)
250
+ })
251
+ }
252
+ }
253
+
254
+ return stuckOps.length
255
+ }
256
+
257
+ /**
258
+ * Checks whether the queue is empty
259
+ */
260
+ isEmpty() {
261
+ return this.queue.length === 0
262
+ }
263
+
264
+ /**
265
+ * Returns information about the next operation in the queue
266
+ */
267
+ peekNext() {
268
+ if (this.queue.length === 0) {
269
+ return null
270
+ }
271
+
272
+ const next = this.queue[0]
273
+ return {
274
+ id: next.id,
275
+ timestamp: next.timestamp,
276
+ waitTime: Date.now() - next.timestamp
277
+ }
278
+ }
279
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * SchemaManager - Manages field schemas for optimized array-based serialization
3
+ * This replaces the need for repeating field names in JSON objects
4
+ */
5
+ export default class SchemaManager {
6
+ constructor(opts = {}) {
7
+ this.opts = Object.assign({
8
+ enableArraySerialization: true,
9
+ strictSchema: true,
10
+ debugMode: false
11
+ }, opts)
12
+
13
+ // Schema definition: array of field names in order
14
+ this.schema = []
15
+ this.fieldToIndex = new Map() // field name -> index
16
+ this.indexToField = new Map() // index -> field name
17
+ this.schemaVersion = 1
18
+ this.isInitialized = false
19
+ }
20
+
21
+ /**
22
+ * Initialize schema from options or auto-detect from data
23
+ */
24
+ initializeSchema(schemaOrData, autoDetect = false) {
25
+ if (this.isInitialized && this.opts.strictSchema) {
26
+ if (this.opts.debugMode) {
27
+ console.log('SchemaManager: Schema already initialized, skipping')
28
+ }
29
+ return
30
+ }
31
+
32
+ if (Array.isArray(schemaOrData)) {
33
+ // Explicit schema provided
34
+ this.setSchema(schemaOrData)
35
+ } else if (autoDetect && typeof schemaOrData === 'object') {
36
+ // Auto-detect schema from data
37
+ this.autoDetectSchema(schemaOrData)
38
+ } else if (schemaOrData && typeof schemaOrData === 'object') {
39
+ // Initialize from database options
40
+ this.initializeFromOptions(schemaOrData)
41
+ }
42
+
43
+ this.isInitialized = true
44
+ if (this.opts.debugMode) {
45
+ console.log('SchemaManager: Schema initialized:', this.schema)
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Set explicit schema
51
+ */
52
+ setSchema(fieldNames) {
53
+ this.schema = [...fieldNames] // Create copy
54
+ this.fieldToIndex.clear()
55
+ this.indexToField.clear()
56
+
57
+ this.schema.forEach((field, index) => {
58
+ this.fieldToIndex.set(field, index)
59
+ this.indexToField.set(index, field)
60
+ })
61
+
62
+ if (this.opts.debugMode) {
63
+ console.log('SchemaManager: Schema set:', this.schema)
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Auto-detect schema from sample data
69
+ */
70
+ autoDetectSchema(sampleData) {
71
+ if (Array.isArray(sampleData)) {
72
+ // Use first record as template
73
+ if (sampleData.length > 0) {
74
+ this.autoDetectSchema(sampleData[0])
75
+ }
76
+ return
77
+ }
78
+
79
+ if (typeof sampleData === 'object' && sampleData !== null) {
80
+ const fields = Object.keys(sampleData).sort() // Sort for consistency
81
+
82
+ // CRITICAL FIX: Always include 'id' field in schema for proper array format
83
+ if (!fields.includes('id')) {
84
+ fields.push('id')
85
+ }
86
+
87
+ this.setSchema(fields)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Initialize schema from database options
93
+ */
94
+ initializeFromOptions(opts) {
95
+ if (opts.schema && Array.isArray(opts.schema)) {
96
+ this.setSchema(opts.schema)
97
+ }
98
+ // CRITICAL FIX: Don't auto-initialize schema from indexes
99
+ // This was causing data loss because only indexed fields were preserved
100
+ // Let schema be auto-detected from actual data instead
101
+ }
102
+
103
+ /**
104
+ * Add new field to schema (for schema evolution)
105
+ */
106
+ addField(fieldName) {
107
+ if (this.fieldToIndex.has(fieldName)) {
108
+ return this.fieldToIndex.get(fieldName)
109
+ }
110
+
111
+ const newIndex = this.schema.length
112
+ this.schema.push(fieldName)
113
+ this.fieldToIndex.set(fieldName, newIndex)
114
+ this.indexToField.set(newIndex, fieldName)
115
+
116
+ if (this.opts.debugMode) {
117
+ console.log('SchemaManager: Added field:', fieldName, 'at index:', newIndex)
118
+ }
119
+
120
+ return newIndex
121
+ }
122
+
123
+ /**
124
+ * Convert object to array using schema with strict field enforcement
125
+ */
126
+ objectToArray(obj) {
127
+ if (!this.isInitialized || !this.opts.enableArraySerialization) {
128
+ return obj // Fallback to object format
129
+ }
130
+
131
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
132
+ return obj // Don't convert non-objects or arrays
133
+ }
134
+
135
+ const result = new Array(this.schema.length)
136
+
137
+ // Fill array with values in schema order
138
+ // Missing fields become undefined, extra fields are ignored
139
+ for (let i = 0; i < this.schema.length; i++) {
140
+ const fieldName = this.schema[i]
141
+ result[i] = obj[fieldName] !== undefined ? obj[fieldName] : undefined
142
+ }
143
+
144
+ return result
145
+ }
146
+
147
+ /**
148
+ * Convert array back to object using schema
149
+ */
150
+ arrayToObject(arr) {
151
+ if (!this.isInitialized || !this.opts.enableArraySerialization) {
152
+ return arr // Fallback to array format
153
+ }
154
+
155
+ if (!Array.isArray(arr)) {
156
+ return arr // Don't convert non-arrays
157
+ }
158
+
159
+ const obj = {}
160
+
161
+ // Map array values to object properties
162
+ // Only include fields that are in the schema
163
+ for (let i = 0; i < Math.min(arr.length, this.schema.length); i++) {
164
+ const fieldName = this.schema[i]
165
+ // Only include non-undefined values to avoid cluttering the object
166
+ if (arr[i] !== undefined) {
167
+ obj[fieldName] = arr[i]
168
+ }
169
+ }
170
+
171
+ return obj
172
+ }
173
+
174
+ /**
175
+ * Get field index by name
176
+ */
177
+ getFieldIndex(fieldName) {
178
+ return this.fieldToIndex.get(fieldName)
179
+ }
180
+
181
+ /**
182
+ * Get field name by index
183
+ */
184
+ getFieldName(index) {
185
+ return this.indexToField.get(index)
186
+ }
187
+
188
+ /**
189
+ * Check if field exists in schema
190
+ */
191
+ hasField(fieldName) {
192
+ return this.fieldToIndex.has(fieldName)
193
+ }
194
+
195
+ /**
196
+ * Get schema as array of field names
197
+ */
198
+ getSchema() {
199
+ return [...this.schema] // Return copy
200
+ }
201
+
202
+ /**
203
+ * Get schema size
204
+ */
205
+ getSchemaSize() {
206
+ return this.schema.length
207
+ }
208
+
209
+ /**
210
+ * Validate that object conforms to schema
211
+ */
212
+ validateObject(obj) {
213
+ if (!this.isInitialized || !this.opts.strictSchema) {
214
+ return true
215
+ }
216
+
217
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
218
+ return false
219
+ }
220
+
221
+ // Check if object has all required fields
222
+ for (const field of this.schema) {
223
+ if (!(field in obj)) {
224
+ if (this.opts.debugMode) {
225
+ console.warn('SchemaManager: Missing required field:', field)
226
+ }
227
+ return false
228
+ }
229
+ }
230
+
231
+ return true
232
+ }
233
+
234
+ /**
235
+ * Get schema metadata for serialization
236
+ */
237
+ getSchemaMetadata() {
238
+ return {
239
+ version: this.schemaVersion,
240
+ fields: [...this.schema],
241
+ fieldCount: this.schema.length,
242
+ isInitialized: this.isInitialized
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Reset schema
248
+ */
249
+ reset() {
250
+ this.schema = []
251
+ this.fieldToIndex.clear()
252
+ this.indexToField.clear()
253
+ this.isInitialized = false
254
+ this.schemaVersion++
255
+ }
256
+
257
+ /**
258
+ * Get performance statistics
259
+ */
260
+ getStats() {
261
+ return {
262
+ schemaSize: this.schema.length,
263
+ isInitialized: this.isInitialized,
264
+ version: this.schemaVersion,
265
+ enableArraySerialization: this.opts.enableArraySerialization
266
+ }
267
+ }
268
+ }