jexidb 2.0.2 → 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.
- package/.babelrc +13 -0
- package/.gitattributes +2 -0
- package/CHANGELOG.md +140 -0
- package/LICENSE +21 -21
- package/README.md +301 -527
- package/babel.config.json +5 -0
- package/dist/Database.cjs +3896 -0
- package/docs/API.md +1051 -0
- package/docs/EXAMPLES.md +701 -0
- package/docs/README.md +194 -0
- package/examples/iterate-usage-example.js +157 -0
- package/examples/simple-iterate-example.js +115 -0
- package/jest.config.js +24 -0
- package/package.json +63 -51
- package/scripts/README.md +47 -0
- package/scripts/clean-test-files.js +75 -0
- package/scripts/prepare.js +31 -0
- package/scripts/run-tests.js +80 -0
- package/src/Database.mjs +4130 -0
- package/src/FileHandler.mjs +1101 -0
- package/src/OperationQueue.mjs +279 -0
- package/src/SchemaManager.mjs +268 -0
- package/src/Serializer.mjs +511 -0
- package/src/managers/ConcurrencyManager.mjs +257 -0
- package/src/managers/IndexManager.mjs +1403 -0
- package/src/managers/QueryManager.mjs +1273 -0
- package/src/managers/StatisticsManager.mjs +262 -0
- package/src/managers/StreamingProcessor.mjs +429 -0
- package/src/managers/TermManager.mjs +278 -0
- package/test/$not-operator-with-and.test.js +282 -0
- package/test/README.md +8 -0
- package/test/close-init-cycle.test.js +256 -0
- package/test/critical-bugs-fixes.test.js +1069 -0
- package/test/index-persistence.test.js +306 -0
- package/test/index-serialization.test.js +314 -0
- package/test/indexed-query-mode.test.js +360 -0
- package/test/iterate-method.test.js +272 -0
- package/test/query-operators.test.js +238 -0
- package/test/regex-array-fields.test.js +129 -0
- package/test/score-method.test.js +238 -0
- package/test/setup.js +17 -0
- package/test/term-mapping-minimal.test.js +154 -0
- package/test/term-mapping-simple.test.js +257 -0
- package/test/term-mapping.test.js +514 -0
- package/test/writebuffer-flush-resilience.test.js +204 -0
- package/dist/FileHandler.js +0 -688
- package/dist/IndexManager.js +0 -353
- package/dist/IntegrityChecker.js +0 -364
- package/dist/JSONLDatabase.js +0 -1194
- package/dist/index.js +0 -617
- package/src/FileHandler.js +0 -674
- package/src/IndexManager.js +0 -363
- package/src/IntegrityChecker.js +0 -379
- package/src/JSONLDatabase.js +0 -1248
- 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
|
+
}
|