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.
- package/.babelrc +13 -0
- package/.gitattributes +2 -0
- package/CHANGELOG.md +132 -101
- package/LICENSE +21 -21
- package/README.md +301 -639
- package/babel.config.json +5 -0
- package/dist/Database.cjs +5204 -0
- package/docs/API.md +908 -241
- package/docs/EXAMPLES.md +701 -177
- package/docs/README.md +194 -184
- 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 -54
- package/scripts/README.md +47 -0
- package/scripts/benchmark-array-serialization.js +108 -0
- package/scripts/clean-test-files.js +75 -0
- package/scripts/prepare.js +31 -0
- package/scripts/run-tests.js +80 -0
- package/scripts/score-mode-demo.js +45 -0
- package/src/Database.mjs +5325 -0
- package/src/FileHandler.mjs +1140 -0
- package/src/OperationQueue.mjs +279 -0
- package/src/SchemaManager.mjs +268 -0
- package/src/Serializer.mjs +702 -0
- package/src/managers/ConcurrencyManager.mjs +257 -0
- package/src/managers/IndexManager.mjs +2094 -0
- package/src/managers/QueryManager.mjs +1490 -0
- package/src/managers/StatisticsManager.mjs +262 -0
- package/src/managers/StreamingProcessor.mjs +429 -0
- package/src/managers/TermManager.mjs +278 -0
- package/src/utils/operatorNormalizer.mjs +116 -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/coverage-method.test.js +93 -0
- package/test/critical-bugs-fixes.test.js +1069 -0
- package/test/deserialize-corruption-fixes.test.js +296 -0
- package/test/exists-method.test.js +318 -0
- package/test/explicit-indexes-comparison.test.js +219 -0
- package/test/filehandler-non-adjacent-ranges-bug.test.js +175 -0
- package/test/index-line-number-regression.test.js +100 -0
- package/test/index-missing-index-data.test.js +91 -0
- package/test/index-persistence.test.js +491 -0
- package/test/index-serialization.test.js +314 -0
- package/test/indexed-query-mode.test.js +360 -0
- package/test/insert-session-auto-flush.test.js +353 -0
- package/test/iterate-method.test.js +272 -0
- package/test/legacy-operator-compat.test.js +154 -0
- package/test/query-operators.test.js +238 -0
- package/test/regex-array-fields.test.js +129 -0
- package/test/score-method.test.js +298 -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 -1333
- package/dist/index.js +0 -617
- package/docs/MIGRATION.md +0 -295
- package/examples/auto-save-example.js +0 -158
- package/examples/cjs-usage.cjs +0 -82
- package/examples/close-vs-delete-example.js +0 -71
- package/examples/esm-usage.js +0 -113
- package/examples/example-columns.idx.jdb +0 -0
- package/examples/example-columns.jdb +0 -9
- package/examples/example-options.idx.jdb +0 -0
- package/examples/example-options.jdb +0 -0
- package/examples/example-users.idx.jdb +0 -0
- package/examples/example-users.jdb +0 -5
- package/examples/simple-test.js +0 -55
- package/src/FileHandler.js +0 -674
- package/src/IndexManager.js +0 -363
- package/src/IntegrityChecker.js +0 -379
- package/src/JSONLDatabase.js +0 -1391
- 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
|
+
}
|