jexidb 2.0.3 → 2.1.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.
Files changed (67) 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 +3896 -0
  8. package/docs/API.md +1051 -390
  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/clean-test-files.js +75 -0
  17. package/scripts/prepare.js +31 -0
  18. package/scripts/run-tests.js +80 -0
  19. package/src/Database.mjs +4130 -0
  20. package/src/FileHandler.mjs +1101 -0
  21. package/src/OperationQueue.mjs +279 -0
  22. package/src/SchemaManager.mjs +268 -0
  23. package/src/Serializer.mjs +511 -0
  24. package/src/managers/ConcurrencyManager.mjs +257 -0
  25. package/src/managers/IndexManager.mjs +1403 -0
  26. package/src/managers/QueryManager.mjs +1273 -0
  27. package/src/managers/StatisticsManager.mjs +262 -0
  28. package/src/managers/StreamingProcessor.mjs +429 -0
  29. package/src/managers/TermManager.mjs +278 -0
  30. package/test/$not-operator-with-and.test.js +282 -0
  31. package/test/README.md +8 -0
  32. package/test/close-init-cycle.test.js +256 -0
  33. package/test/critical-bugs-fixes.test.js +1069 -0
  34. package/test/index-persistence.test.js +306 -0
  35. package/test/index-serialization.test.js +314 -0
  36. package/test/indexed-query-mode.test.js +360 -0
  37. package/test/iterate-method.test.js +272 -0
  38. package/test/query-operators.test.js +238 -0
  39. package/test/regex-array-fields.test.js +129 -0
  40. package/test/score-method.test.js +238 -0
  41. package/test/setup.js +17 -0
  42. package/test/term-mapping-minimal.test.js +154 -0
  43. package/test/term-mapping-simple.test.js +257 -0
  44. package/test/term-mapping.test.js +514 -0
  45. package/test/writebuffer-flush-resilience.test.js +204 -0
  46. package/dist/FileHandler.js +0 -688
  47. package/dist/IndexManager.js +0 -353
  48. package/dist/IntegrityChecker.js +0 -364
  49. package/dist/JSONLDatabase.js +0 -1333
  50. package/dist/index.js +0 -617
  51. package/docs/MIGRATION.md +0 -295
  52. package/examples/auto-save-example.js +0 -158
  53. package/examples/cjs-usage.cjs +0 -82
  54. package/examples/close-vs-delete-example.js +0 -71
  55. package/examples/esm-usage.js +0 -113
  56. package/examples/example-columns.idx.jdb +0 -0
  57. package/examples/example-columns.jdb +0 -9
  58. package/examples/example-options.idx.jdb +0 -0
  59. package/examples/example-options.jdb +0 -0
  60. package/examples/example-users.idx.jdb +0 -0
  61. package/examples/example-users.jdb +0 -5
  62. package/examples/simple-test.js +0 -55
  63. package/src/FileHandler.js +0 -674
  64. package/src/IndexManager.js +0 -363
  65. package/src/IntegrityChecker.js +0 -379
  66. package/src/JSONLDatabase.js +0 -1391
  67. package/src/index.js +0 -608
@@ -0,0 +1,279 @@
1
+ /**
2
+ * OperationQueue - Sistema de fila para operações do banco de dados
3
+ * Resolve race conditions entre operações concorrentes
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
+ * Adiciona uma operação à fila
24
+ * @param {Function} operation - Função assíncrona a ser executada
25
+ * @returns {Promise} - Promise que resolve quando a operação é concluída
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
+ // Processar imediatamente se não estiver processando
52
+ this.process().catch(reject)
53
+ })
54
+ }
55
+
56
+ /**
57
+ * Processa todas as operações na fila sequencialmente
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
+ * Aguarda todas as operações pendentes serem processadas
120
+ * @param {number|null} maxWaitTime - Tempo máximo de espera em ms (null = wait indefinitely)
121
+ * @returns {Promise<boolean>} - true se todas foram processadas, false se timeout
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
+ * Retorna o tamanho atual da fila
171
+ */
172
+ getQueueLength() {
173
+ return this.queue.length
174
+ }
175
+
176
+ /**
177
+ * Verifica se está processando operações
178
+ */
179
+ isProcessing() {
180
+ return this.processing
181
+ }
182
+
183
+ /**
184
+ * Retorna estatísticas da fila
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
+ * Limpa a fila (para casos de emergência)
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
+ * Detecta operações travadas e retorna informações detalhadas
212
+ * @param {number} stuckThreshold - Tempo em ms para considerar uma operação travada
213
+ * @returns {Array} - Lista de operações travadas com 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
+ * Força a limpeza de operações travadas (último recurso)
229
+ * @param {number} stuckThreshold - Tempo em ms para considerar uma operação travada
230
+ * @returns {number} - Número de operações removidas
231
+ */
232
+ forceCleanupStuckOperations(stuckThreshold = 10000) {
233
+ const stuckOps = this.detectStuckOperations(stuckThreshold)
234
+
235
+ if (stuckOps.length > 0) {
236
+ // Rejeitar todas as operações travadas
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
+ * Verifica se a fila está vazia
259
+ */
260
+ isEmpty() {
261
+ return this.queue.length === 0
262
+ }
263
+
264
+ /**
265
+ * Retorna informações sobre a próxima operação na fila
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
+ }