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.
Files changed (55) hide show
  1. package/.babelrc +13 -0
  2. package/.gitattributes +2 -0
  3. package/CHANGELOG.md +140 -0
  4. package/LICENSE +21 -21
  5. package/README.md +301 -527
  6. package/babel.config.json +5 -0
  7. package/dist/Database.cjs +3896 -0
  8. package/docs/API.md +1051 -0
  9. package/docs/EXAMPLES.md +701 -0
  10. package/docs/README.md +194 -0
  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 -51
  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 -1194
  50. package/dist/index.js +0 -617
  51. package/src/FileHandler.js +0 -674
  52. package/src/IndexManager.js +0 -363
  53. package/src/IntegrityChecker.js +0 -379
  54. package/src/JSONLDatabase.js +0 -1248
  55. package/src/index.js +0 -608
@@ -0,0 +1,3896 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = exports.Database = void 0;
7
+ var _events = require("events");
8
+ var _IndexManager = _interopRequireDefault(require("./managers/IndexManager.mjs"));
9
+ var _Serializer = _interopRequireDefault(require("./Serializer.mjs"));
10
+ var _asyncMutex = require("async-mutex");
11
+ var _fs = _interopRequireDefault(require("fs"));
12
+ var _readline = _interopRequireDefault(require("readline"));
13
+ var _OperationQueue = require("./OperationQueue.mjs");
14
+ var _FileHandler = _interopRequireDefault(require("./FileHandler.mjs"));
15
+ var _QueryManager = require("./managers/QueryManager.mjs");
16
+ var _ConcurrencyManager = require("./managers/ConcurrencyManager.mjs");
17
+ var _StatisticsManager = require("./managers/StatisticsManager.mjs");
18
+ var _StreamingProcessor = _interopRequireDefault(require("./managers/StreamingProcessor.mjs"));
19
+ var _TermManager = _interopRequireDefault(require("./managers/TermManager.mjs"));
20
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
21
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
22
+ function _awaitAsyncGenerator(e) { return new _OverloadYield(e, 0); }
23
+ function _wrapAsyncGenerator(e) { return function () { return new AsyncGenerator(e.apply(this, arguments)); }; }
24
+ function AsyncGenerator(e) { var r, t; function resume(r, t) { try { var n = e[r](t), o = n.value, u = o instanceof _OverloadYield; Promise.resolve(u ? o.v : o).then(function (t) { if (u) { var i = "return" === r ? "return" : "next"; if (!o.k || t.done) return resume(i, t); t = e[i](t).value; } settle(n.done ? "return" : "normal", t); }, function (e) { resume("throw", e); }); } catch (e) { settle("throw", e); } } function settle(e, n) { switch (e) { case "return": r.resolve({ value: n, done: !0 }); break; case "throw": r.reject(n); break; default: r.resolve({ value: n, done: !1 }); } (r = r.next) ? resume(r.key, r.arg) : t = null; } this._invoke = function (e, n) { return new Promise(function (o, u) { var i = { key: e, arg: n, resolve: o, reject: u, next: null }; t ? t = t.next = i : (r = t = i, resume(e, n)); }); }, "function" != typeof e.return && (this.return = void 0); }
25
+ AsyncGenerator.prototype["function" == typeof Symbol && Symbol.asyncIterator || "@@asyncIterator"] = function () { return this; }, AsyncGenerator.prototype.next = function (e) { return this._invoke("next", e); }, AsyncGenerator.prototype.throw = function (e) { return this._invoke("throw", e); }, AsyncGenerator.prototype.return = function (e) { return this._invoke("return", e); };
26
+ function _OverloadYield(e, d) { this.v = e, this.k = d; }
27
+ function _asyncIterator(r) { var n, t, o, e = 2; for ("undefined" != typeof Symbol && (t = Symbol.asyncIterator, o = Symbol.iterator); e--;) { if (t && null != (n = r[t])) return n.call(r); if (o && null != (n = r[o])) return new AsyncFromSyncIterator(n.call(r)); t = "@@asyncIterator", o = "@@iterator"; } throw new TypeError("Object is not async iterable"); }
28
+ function AsyncFromSyncIterator(r) { function AsyncFromSyncIteratorContinuation(r) { if (Object(r) !== r) return Promise.reject(new TypeError(r + " is not an object.")); var n = r.done; return Promise.resolve(r.value).then(function (r) { return { value: r, done: n }; }); } return AsyncFromSyncIterator = function (r) { this.s = r, this.n = r.next; }, AsyncFromSyncIterator.prototype = { s: null, n: null, next: function () { return AsyncFromSyncIteratorContinuation(this.n.apply(this.s, arguments)); }, return: function (r) { var n = this.s.return; return void 0 === n ? Promise.resolve({ value: r, done: !0 }) : AsyncFromSyncIteratorContinuation(n.apply(this.s, arguments)); }, throw: function (r) { var n = this.s.return; return void 0 === n ? Promise.reject(r) : AsyncFromSyncIteratorContinuation(n.apply(this.s, arguments)); } }, new AsyncFromSyncIterator(r); }
29
+ /**
30
+ * IterateEntry class for intuitive API with automatic change detection
31
+ * Uses native JavaScript setters for maximum performance
32
+ */
33
+ class IterateEntry {
34
+ constructor(entry, originalRecord) {
35
+ this._entry = entry;
36
+ this._originalRecord = originalRecord;
37
+ this._modified = false;
38
+ this._markedForDeletion = false;
39
+ }
40
+
41
+ // Generic getter that returns values from the original entry
42
+ get(property) {
43
+ return this._entry[property];
44
+ }
45
+
46
+ // Generic setter that sets values in the original entry
47
+ set(property, value) {
48
+ this._entry[property] = value;
49
+ this._modified = true;
50
+ }
51
+
52
+ // Delete method for intuitive deletion
53
+ delete() {
54
+ this._markedForDeletion = true;
55
+ return true;
56
+ }
57
+
58
+ // Getter for the underlying entry (for compatibility)
59
+ get value() {
60
+ return this._entry;
61
+ }
62
+
63
+ // Check if entry was modified
64
+ get isModified() {
65
+ return this._modified;
66
+ }
67
+
68
+ // Check if entry is marked for deletion
69
+ get isMarkedForDeletion() {
70
+ return this._markedForDeletion;
71
+ }
72
+
73
+ // Proxy all property access to the underlying entry
74
+ get [Symbol.toPrimitive]() {
75
+ return this._entry;
76
+ }
77
+
78
+ // Handle property access dynamically
79
+ get [Symbol.toStringTag]() {
80
+ return 'IterateEntry';
81
+ }
82
+ }
83
+
84
+ // Import managers
85
+
86
+ /**
87
+ * InsertSession - Simple batch insertion without memory duplication
88
+ */
89
+ class InsertSession {
90
+ constructor(database, sessionOptions = {}) {
91
+ this.database = database;
92
+ this.batchSize = sessionOptions.batchSize || 100;
93
+ this.totalInserted = 0;
94
+ this.flushing = false;
95
+ this.batches = []; // Array of batches to avoid slice() in flush()
96
+ this.currentBatch = []; // Current batch being filled
97
+ this.sessionId = Math.random().toString(36).substr(2, 9);
98
+
99
+ // Register this session as active
100
+ this.database.activeInsertSessions.add(this);
101
+ }
102
+ async add(record) {
103
+ // CRITICAL FIX: Remove the committed check to allow auto-reusability
104
+ // The session should be able to handle multiple commits
105
+
106
+ if (this.database.destroyed) {
107
+ throw new Error('Database is destroyed');
108
+ }
109
+
110
+ // Process record
111
+ const finalRecord = {
112
+ ...record
113
+ };
114
+ const id = finalRecord.id || this.database.generateId();
115
+ finalRecord.id = id;
116
+
117
+ // Add to current batch
118
+ this.currentBatch.push(finalRecord);
119
+ this.totalInserted++;
120
+
121
+ // If batch is full, move it to batches array
122
+ if (this.currentBatch.length >= this.batchSize) {
123
+ this.batches.push(this.currentBatch);
124
+ this.currentBatch = [];
125
+ }
126
+ return finalRecord;
127
+ }
128
+ async flush() {
129
+ // Check if there's anything to flush
130
+ if (this.batches.length === 0 && this.currentBatch.length === 0) return;
131
+
132
+ // Prevent concurrent flushes
133
+ if (this.flushing) return;
134
+ this.flushing = true;
135
+ try {
136
+ // Process all complete batches
137
+ for (const batch of this.batches) {
138
+ await this.database.insertBatch(batch);
139
+ }
140
+
141
+ // Process remaining records in current batch
142
+ if (this.currentBatch.length > 0) {
143
+ await this.database.insertBatch(this.currentBatch);
144
+ }
145
+
146
+ // Clear all batches
147
+ this.batches = [];
148
+ this.currentBatch = [];
149
+ } finally {
150
+ this.flushing = false;
151
+ }
152
+ }
153
+ async commit() {
154
+ // CRITICAL FIX: Make session auto-reusable by removing committed state
155
+ // Allow multiple commits on the same session
156
+
157
+ await this.flush();
158
+
159
+ // Reset session state for next commit cycle
160
+ const insertedCount = this.totalInserted;
161
+ this.totalInserted = 0;
162
+ return insertedCount;
163
+ }
164
+
165
+ /**
166
+ * Wait for this session's operations to complete
167
+ */
168
+ async waitForOperations(maxWaitTime = null) {
169
+ const startTime = Date.now();
170
+ const hasTimeout = maxWaitTime !== null && maxWaitTime !== undefined;
171
+ while (this.flushing || this.batches.length > 0 || this.currentBatch.length > 0) {
172
+ // Check timeout only if we have one
173
+ if (hasTimeout && Date.now() - startTime >= maxWaitTime) {
174
+ return false;
175
+ }
176
+ await new Promise(resolve => setTimeout(resolve, 1));
177
+ }
178
+ return true;
179
+ }
180
+
181
+ /**
182
+ * Check if this session has pending operations
183
+ */
184
+ hasPendingOperations() {
185
+ return this.flushing || this.batches.length > 0 || this.currentBatch.length > 0;
186
+ }
187
+
188
+ /**
189
+ * Destroy this session and unregister it
190
+ */
191
+ destroy() {
192
+ // Unregister from database
193
+ this.database.activeInsertSessions.delete(this);
194
+
195
+ // Clear all data
196
+ this.batches = [];
197
+ this.currentBatch = [];
198
+ this.totalInserted = 0;
199
+ this.flushing = false;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * JexiDB - A high-performance, in-memory database with persistence
205
+ *
206
+ * Features:
207
+ * - In-memory storage with optional persistence
208
+ * - Advanced indexing and querying
209
+ * - Transaction support
210
+ * - Manual save functionality
211
+ * - Recovery mechanisms
212
+ * - Performance optimizations
213
+ */
214
+ class Database extends _events.EventEmitter {
215
+ constructor(file, opts = {}) {
216
+ super();
217
+
218
+ // Generate unique instance ID for debugging
219
+ this.instanceId = Math.random().toString(36).substr(2, 9);
220
+
221
+ // Initialize state flags
222
+ this.managersInitialized = false;
223
+
224
+ // Track active insert sessions
225
+ this.activeInsertSessions = new Set();
226
+
227
+ // Set default options
228
+ this.opts = Object.assign({
229
+ // Core options - auto-save removed, user must call save() manually
230
+ // File creation options
231
+ create: opts.create !== false,
232
+ // Create file if it doesn't exist (default true)
233
+ clear: opts.clear === true,
234
+ // Clear existing files before loading (default false)
235
+ // Timeout configurations for preventing hangs
236
+ mutexTimeout: opts.mutexTimeout || 15000,
237
+ // 15 seconds timeout for mutex operations
238
+ maxFlushAttempts: opts.maxFlushAttempts || 50,
239
+ // Maximum flush attempts before giving up
240
+ // Term mapping options (always enabled and auto-detected from indexes)
241
+ termMappingCleanup: opts.termMappingCleanup !== false,
242
+ // Clean up orphaned terms on save (enabled by default)
243
+ // Recovery options
244
+ enableRecovery: opts.enableRecovery === true,
245
+ // Recovery mechanisms disabled by default for large databases
246
+ // Buffer size options for range merging
247
+ maxBufferSize: opts.maxBufferSize || 4 * 1024 * 1024,
248
+ // 4MB default maximum buffer size for grouped ranges
249
+ // Memory management options (similar to published v1.1.0)
250
+ maxMemoryUsage: opts.maxMemoryUsage || 64 * 1024,
251
+ // 64KB limit like published version
252
+ maxWriteBufferSize: opts.maxWriteBufferSize || 1000,
253
+ // Maximum records in writeBuffer
254
+ // Query strategy options
255
+ streamingThreshold: opts.streamingThreshold || 0.8,
256
+ // Use streaming when limit > 80% of total records
257
+ // Serialization options
258
+ enableArraySerialization: opts.enableArraySerialization !== false // Enable array serialization by default
259
+ }, opts);
260
+
261
+ // CRITICAL FIX: Initialize AbortController for lifecycle management
262
+ this.abortController = new AbortController();
263
+ this.pendingOperations = new Set();
264
+ this.pendingPromises = new Set();
265
+ this.destroyed = false;
266
+ this.destroying = false;
267
+ this.closed = false;
268
+ this.operationCounter = 0;
269
+
270
+ // CRITICAL FIX: Initialize OperationQueue to prevent race conditions
271
+ this.operationQueue = new _OperationQueue.OperationQueue(false); // Disable debug mode for queue
272
+
273
+ // Normalize file path to ensure it ends with .jdb
274
+ this.normalizedFile = this.normalizeFilePath(file);
275
+
276
+ // Initialize core properties
277
+ this.offsets = []; // Array of byte offsets for each record
278
+ this.indexOffset = 0; // Current position in file for new records
279
+ this.deletedIds = new Set(); // Track deleted record IDs
280
+ this.shouldSave = false;
281
+ this.isLoading = false;
282
+ this.isSaving = false;
283
+ this.lastSaveTime = null;
284
+ this.initialized = false;
285
+
286
+ // Initialize managers
287
+ this.initializeManagers();
288
+
289
+ // Initialize file mutex for thread safety
290
+ this.fileMutex = new _asyncMutex.Mutex();
291
+
292
+ // Initialize performance tracking
293
+ this.performanceStats = {
294
+ operations: 0,
295
+ saves: 0,
296
+ loads: 0,
297
+ queryTime: 0,
298
+ saveTime: 0,
299
+ loadTime: 0
300
+ };
301
+
302
+ // Initialize usage stats for QueryManager
303
+ this.usageStats = {
304
+ totalQueries: 0,
305
+ indexedQueries: 0,
306
+ streamingQueries: 0,
307
+ indexedAverageTime: 0,
308
+ streamingAverageTime: 0
309
+ };
310
+
311
+ // Note: Validation will be done after configuration conversion in initializeManagers()
312
+ }
313
+
314
+ /**
315
+ * Validate field and index configuration
316
+ */
317
+ validateIndexConfiguration() {
318
+ // Validate fields configuration
319
+ if (this.opts.fields && typeof this.opts.fields === 'object') {
320
+ this.validateFieldTypes(this.opts.fields, 'fields');
321
+ }
322
+
323
+ // Validate indexes configuration (legacy support)
324
+ if (this.opts.indexes && typeof this.opts.indexes === 'object') {
325
+ this.validateFieldTypes(this.opts.indexes, 'indexes');
326
+ }
327
+
328
+ // Validate indexes array (new format) - but only if we have fields
329
+ if (this.opts.originalIndexes && Array.isArray(this.opts.originalIndexes)) {
330
+ if (!this.opts.fields) {
331
+ throw new Error('Index fields array requires fields configuration. Use: { fields: {...}, indexes: [...] }');
332
+ }
333
+ this.validateIndexFields(this.opts.originalIndexes);
334
+ }
335
+ if (this.opts.debugMode) {
336
+ const fieldCount = this.opts.fields ? Object.keys(this.opts.fields).length : 0;
337
+ const indexCount = Array.isArray(this.opts.indexes) ? this.opts.indexes.length : this.opts.indexes && typeof this.opts.indexes === 'object' ? Object.keys(this.opts.indexes).length : 0;
338
+ if (fieldCount > 0 || indexCount > 0) {
339
+ console.log(`✅ Configuration validated: ${fieldCount} fields, ${indexCount} indexes [${this.instanceId}]`);
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Validate field types
346
+ */
347
+ validateFieldTypes(fields, configType) {
348
+ const supportedTypes = ['string', 'number', 'boolean', 'array:string', 'array:number', 'array:boolean', 'array', 'object'];
349
+ const errors = [];
350
+ for (const [fieldName, fieldType] of Object.entries(fields)) {
351
+ // Check if type is supported
352
+ if (!supportedTypes.includes(fieldType)) {
353
+ errors.push(`Unsupported ${configType} type '${fieldType}' for field '${fieldName}'. Supported types: ${supportedTypes.join(', ')}`);
354
+ }
355
+
356
+ // Warn about legacy array type but don't error
357
+ if (fieldType === 'array') {
358
+ if (this.opts.debugMode) {
359
+ console.log(`⚠️ Legacy array type '${fieldType}' for field '${fieldName}'. Consider using 'array:string' for better performance.`);
360
+ }
361
+ }
362
+
363
+ // Check for common mistakes
364
+ if (fieldType === 'array:') {
365
+ errors.push(`Incomplete array type '${fieldType}' for field '${fieldName}'. Must specify element type after colon: array:string, array:number, or array:boolean`);
366
+ }
367
+ }
368
+ if (errors.length > 0) {
369
+ throw new Error(`${configType.charAt(0).toUpperCase() + configType.slice(1)} configuration errors:\n${errors.map(e => ` - ${e}`).join('\n')}`);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Validate index fields array
375
+ */
376
+ validateIndexFields(indexFields) {
377
+ if (!this.opts.fields) {
378
+ throw new Error('Index fields array requires fields configuration. Use: { fields: {...}, indexes: [...] }');
379
+ }
380
+ const availableFields = Object.keys(this.opts.fields);
381
+ const errors = [];
382
+ for (const fieldName of indexFields) {
383
+ if (!availableFields.includes(fieldName)) {
384
+ errors.push(`Index field '${fieldName}' not found in fields configuration. Available fields: ${availableFields.join(', ')}`);
385
+ }
386
+ }
387
+ if (errors.length > 0) {
388
+ throw new Error(`Index configuration errors:\n${errors.map(e => ` - ${e}`).join('\n')}`);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Prepare index configuration for IndexManager
394
+ */
395
+ prepareIndexConfiguration() {
396
+ // Convert new fields/indexes format to legacy format for IndexManager
397
+ if (this.opts.fields && Array.isArray(this.opts.indexes)) {
398
+ // New format: { fields: {...}, indexes: [...] }
399
+ const indexedFields = {};
400
+ const originalIndexes = [...this.opts.indexes]; // Keep original for validation
401
+
402
+ for (const fieldName of this.opts.indexes) {
403
+ if (this.opts.fields[fieldName]) {
404
+ indexedFields[fieldName] = this.opts.fields[fieldName];
405
+ }
406
+ }
407
+
408
+ // Store original indexes for validation
409
+ this.opts.originalIndexes = originalIndexes;
410
+
411
+ // Replace indexes array with object for IndexManager
412
+ this.opts.indexes = indexedFields;
413
+ if (this.opts.debugMode) {
414
+ console.log(`🔍 Converted fields/indexes format: ${Object.keys(indexedFields).join(', ')} [${this.instanceId}]`);
415
+ }
416
+ }
417
+ // Legacy format (indexes as object) is already compatible
418
+ }
419
+
420
+ /**
421
+ * Initialize all managers
422
+ */
423
+ initializeManagers() {
424
+ // CRITICAL FIX: Prevent double initialization which corrupts term mappings
425
+ if (this.managersInitialized) {
426
+ if (this.opts.debugMode) {
427
+ console.log(`⚠️ initializeManagers() called again - skipping to prevent corruption [${this.instanceId}]`);
428
+ }
429
+ return;
430
+ }
431
+
432
+ // CRITICAL FIX: Initialize serializer first - this was missing and causing crashes
433
+ this.serializer = new _Serializer.default(this.opts);
434
+
435
+ // Initialize schema for array-based serialization
436
+ if (this.opts.enableArraySerialization !== false) {
437
+ this.initializeSchema();
438
+ }
439
+
440
+ // Initialize TermManager - always enabled for optimal performance
441
+ this.termManager = new _TermManager.default();
442
+
443
+ // Auto-detect term mapping fields from indexes
444
+ const termMappingFields = this.getTermMappingFields();
445
+ this.termManager.termMappingFields = termMappingFields;
446
+ this.opts.termMapping = true; // Always enable term mapping for optimal performance
447
+
448
+ if (this.opts.debugMode) {
449
+ if (termMappingFields.length > 0) {
450
+ console.log(`🔍 TermManager initialized for fields: ${termMappingFields.join(', ')} [${this.instanceId}]`);
451
+ } else {
452
+ console.log(`🔍 TermManager initialized (no array:string fields detected) [${this.instanceId}]`);
453
+ }
454
+ }
455
+
456
+ // Prepare index configuration for IndexManager
457
+ this.prepareIndexConfiguration();
458
+
459
+ // Validate configuration after conversion
460
+ this.validateIndexConfiguration();
461
+
462
+ // Initialize IndexManager with database reference for term mapping
463
+ this.indexManager = new _IndexManager.default(this.opts, null, this);
464
+ if (this.opts.debugMode) {
465
+ console.log(`🔍 IndexManager initialized with fields: ${this.indexManager.indexedFields.join(', ')} [${this.instanceId}]`);
466
+ }
467
+
468
+ // Mark managers as initialized
469
+ this.managersInitialized = true;
470
+ this.indexOffset = 0;
471
+ this.writeBuffer = [];
472
+ this.writeBufferOffsets = []; // Track offsets for writeBuffer records
473
+ this.writeBufferSizes = []; // Track sizes for writeBuffer records
474
+ this.isInsideOperationQueue = false; // Flag to prevent deadlock in save() calls
475
+
476
+ // Initialize other managers
477
+ this.fileHandler = new _FileHandler.default(this.normalizedFile, this.fileMutex, this.opts);
478
+ this.queryManager = new _QueryManager.QueryManager(this);
479
+ this.concurrencyManager = new _ConcurrencyManager.ConcurrencyManager(this.opts);
480
+ this.statisticsManager = new _StatisticsManager.StatisticsManager(this, this.opts);
481
+ this.streamingProcessor = new _StreamingProcessor.default(this.opts);
482
+ }
483
+
484
+ /**
485
+ * Get term mapping fields from indexes (auto-detected)
486
+ * @returns {string[]} Array of field names that use term mapping
487
+ */
488
+ getTermMappingFields() {
489
+ if (!this.opts.indexes) return [];
490
+
491
+ // Auto-detect fields that benefit from term mapping
492
+ const termMappingFields = [];
493
+ for (const [field, type] of Object.entries(this.opts.indexes)) {
494
+ // Fields that should use term mapping
495
+ if (type === 'array:string' || type === 'string') {
496
+ termMappingFields.push(field);
497
+ }
498
+ }
499
+ return termMappingFields;
500
+ }
501
+
502
+ /**
503
+ * CRITICAL FIX: Validate database state before critical operations
504
+ * Prevents crashes from undefined methods and invalid states
505
+ */
506
+ validateState() {
507
+ if (this.destroyed) {
508
+ throw new Error('Database is destroyed');
509
+ }
510
+ if (this.closed) {
511
+ throw new Error('Database is closed. Call init() to reopen it.');
512
+ }
513
+
514
+ // Allow operations during destroying phase for proper cleanup
515
+
516
+ if (!this.serializer) {
517
+ throw new Error('Database serializer not initialized - this indicates a critical bug');
518
+ }
519
+ if (!this.normalizedFile) {
520
+ throw new Error('Database file path not set - this indicates file path management failure');
521
+ }
522
+ if (!this.fileHandler) {
523
+ throw new Error('Database file handler not initialized');
524
+ }
525
+ if (!this.indexManager) {
526
+ throw new Error('Database index manager not initialized');
527
+ }
528
+ return true;
529
+ }
530
+
531
+ /**
532
+ * CRITICAL FIX: Ensure file path is valid and accessible
533
+ * Prevents file path loss issues mentioned in crash report
534
+ */
535
+ ensureFilePath() {
536
+ if (!this.normalizedFile) {
537
+ throw new Error('Database file path is missing after initialization - this indicates a critical file path management failure');
538
+ }
539
+ return this.normalizedFile;
540
+ }
541
+
542
+ /**
543
+ * Normalize file path to ensure it ends with .jdb
544
+ */
545
+ normalizeFilePath(file) {
546
+ if (!file) return null;
547
+ return file.endsWith('.jdb') ? file : `${file}.jdb`;
548
+ }
549
+
550
+ /**
551
+ * Initialize the database
552
+ */
553
+ async initialize() {
554
+ // Check if database is destroyed first (before checking initialized)
555
+ if (this.destroyed) {
556
+ throw new Error('Cannot initialize destroyed database. Use a new instance instead.');
557
+ }
558
+ if (this.initialized) return;
559
+
560
+ // Prevent concurrent initialization - wait for ongoing init to complete
561
+ if (this.isLoading) {
562
+ if (this.opts.debugMode) {
563
+ console.log('🔄 init() already in progress - waiting for completion');
564
+ }
565
+ // Wait for ongoing initialization to complete
566
+ while (this.isLoading) {
567
+ await new Promise(resolve => setTimeout(resolve, 10));
568
+ }
569
+ // Check if initialization completed successfully
570
+ if (this.initialized) {
571
+ if (this.opts.debugMode) {
572
+ console.log('✅ Concurrent init() completed - database is now initialized');
573
+ }
574
+ return;
575
+ }
576
+ // If we get here, initialization failed - we can try again
577
+ }
578
+ try {
579
+ this.isLoading = true;
580
+
581
+ // Reset closed state when reinitializing
582
+ this.closed = false;
583
+
584
+ // Initialize managers (protected against double initialization)
585
+ this.initializeManagers();
586
+
587
+ // Handle clear option - delete existing files before loading
588
+ if (this.opts.clear && this.normalizedFile) {
589
+ await this.clearExistingFiles();
590
+ }
591
+
592
+ // Check file existence and handle create option
593
+ if (this.normalizedFile) {
594
+ const fileExists = await this.fileHandler.exists();
595
+ if (!fileExists) {
596
+ if (!this.opts.create) {
597
+ throw new Error(`Database file '${this.normalizedFile}' does not exist and create option is disabled`);
598
+ }
599
+ // File will be created when first data is written
600
+ } else {
601
+ // Load existing data if file exists
602
+ await this.load();
603
+ }
604
+ }
605
+
606
+ // Manual save is now the default behavior
607
+
608
+ this.initialized = true;
609
+ this.emit('initialized');
610
+ if (this.opts.debugMode) {
611
+ console.log(`✅ Database initialized with ${this.writeBuffer.length} records`);
612
+ }
613
+ } catch (error) {
614
+ console.error('Failed to initialize database:', error);
615
+ throw error;
616
+ } finally {
617
+ this.isLoading = false;
618
+ }
619
+ }
620
+
621
+ /**
622
+ * Validate that the database is initialized before performing operations
623
+ * @param {string} operation - The operation being attempted
624
+ * @throws {Error} If database is not initialized
625
+ */
626
+ _validateInitialization(operation) {
627
+ if (this.destroyed) {
628
+ throw new Error(`❌ Cannot perform '${operation}' on a destroyed database. Create a new instance instead.`);
629
+ }
630
+ if (this.closed) {
631
+ throw new Error(`❌ Database is closed. Call 'await db.init()' to reopen it before performing '${operation}' operations.`);
632
+ }
633
+ if (!this.initialized) {
634
+ const errorMessage = `❌ Database not initialized. Call 'await db.init()' before performing '${operation}' operations.\n\n` + `Example:\n` + ` const db = new Database('./myfile.jdb')\n` + ` await db.init() // ← Required before any operations\n` + ` await db.insert({ name: 'test' }) // ← Now you can use database operations\n\n` + `File: ${this.normalizedFile || 'unknown'}`;
635
+ throw new Error(errorMessage);
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Clear existing database files (.jdb and .idx.jdb)
641
+ */
642
+ async clearExistingFiles() {
643
+ if (!this.normalizedFile) return;
644
+ try {
645
+ // Clear main database file
646
+ if (await this.fileHandler.exists()) {
647
+ await this.fileHandler.delete();
648
+ if (this.opts.debugMode) {
649
+ console.log(`🗑️ Cleared database file: ${this.normalizedFile}`);
650
+ }
651
+ }
652
+
653
+ // Clear index file
654
+ const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
655
+ const idxFileHandler = new _FileHandler.default(idxPath, this.fileMutex, this.opts);
656
+ if (await idxFileHandler.exists()) {
657
+ await idxFileHandler.delete();
658
+ if (this.opts.debugMode) {
659
+ console.log(`🗑️ Cleared index file: ${idxPath}`);
660
+ }
661
+ }
662
+
663
+ // Reset internal state
664
+ this.offsets = [];
665
+ this.indexOffset = 0;
666
+ this.deletedIds.clear();
667
+ this.shouldSave = false;
668
+
669
+ // Create empty files to ensure they exist
670
+ await this.fileHandler.writeAll('');
671
+ await idxFileHandler.writeAll('');
672
+ if (this.opts.debugMode) {
673
+ console.log('🗑️ Database cleared successfully');
674
+ }
675
+ } catch (error) {
676
+ console.error('Failed to clear existing files:', error);
677
+ throw error;
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Load data from file
683
+ */
684
+ async load() {
685
+ if (!this.normalizedFile) return;
686
+ try {
687
+ const startTime = Date.now();
688
+ this.isLoading = true;
689
+
690
+ // Don't load the entire file - just initialize empty state
691
+ // The actual record count will come from loaded offsets
692
+ this.writeBuffer = []; // writeBuffer is only for new unsaved records
693
+
694
+ // recordCount will be determined from loaded offsets
695
+ // If no offsets were loaded, we'll count records only if needed
696
+
697
+ // Load index data if available (always try to load offsets, even without indexed fields)
698
+ if (this.indexManager) {
699
+ const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
700
+ try {
701
+ const idxFileHandler = new _FileHandler.default(idxPath, this.fileMutex, this.opts);
702
+ const idxData = await idxFileHandler.readAll();
703
+ if (idxData && idxData.trim()) {
704
+ const parsedIdxData = JSON.parse(idxData);
705
+
706
+ // Always load offsets if available (even without indexed fields)
707
+ if (parsedIdxData.offsets && Array.isArray(parsedIdxData.offsets)) {
708
+ this.offsets = parsedIdxData.offsets;
709
+ if (this.opts.debugMode) {
710
+ console.log(`📂 Loaded ${this.offsets.length} offsets from ${idxPath}`);
711
+ }
712
+ }
713
+
714
+ // Load indexOffset for proper range calculations
715
+ if (parsedIdxData.indexOffset !== undefined) {
716
+ this.indexOffset = parsedIdxData.indexOffset;
717
+ if (this.opts.debugMode) {
718
+ console.log(`📂 Loaded indexOffset: ${this.indexOffset} from ${idxPath}`);
719
+ }
720
+ }
721
+
722
+ // Load index data only if available and we have indexed fields
723
+ if (parsedIdxData && parsedIdxData.index && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
724
+ this.indexManager.load(parsedIdxData.index);
725
+
726
+ // Load term mapping data from .idx file if it exists
727
+ if (parsedIdxData.termMapping && this.termManager) {
728
+ await this.termManager.loadTerms(parsedIdxData.termMapping);
729
+ if (this.opts.debugMode) {
730
+ console.log(`📂 Loaded term mapping from ${idxPath}`);
731
+ }
732
+ }
733
+ if (this.opts.debugMode) {
734
+ console.log(`📂 Loaded index data from ${idxPath}`);
735
+ }
736
+ }
737
+
738
+ // Load configuration from .idx file if database exists
739
+ if (parsedIdxData.config) {
740
+ const config = parsedIdxData.config;
741
+
742
+ // Override constructor options with saved configuration
743
+ if (config.fields) {
744
+ this.opts.fields = config.fields;
745
+ if (this.opts.debugMode) {
746
+ console.log(`📂 Loaded fields config from ${idxPath}:`, Object.keys(config.fields));
747
+ }
748
+ }
749
+ if (config.indexes) {
750
+ this.opts.indexes = config.indexes;
751
+ if (this.opts.debugMode) {
752
+ console.log(`📂 Loaded indexes config from ${idxPath}:`, Object.keys(config.indexes));
753
+ }
754
+ }
755
+ if (config.originalIndexes) {
756
+ this.opts.originalIndexes = config.originalIndexes;
757
+ if (this.opts.debugMode) {
758
+ console.log(`📂 Loaded originalIndexes config from ${idxPath}:`, config.originalIndexes.length, 'indexes');
759
+ }
760
+ }
761
+
762
+ // Reinitialize schema from saved configuration
763
+ if (config.schema && this.serializer) {
764
+ this.serializer.initializeSchema(config.schema);
765
+ if (this.opts.debugMode) {
766
+ console.log(`📂 Loaded schema from ${idxPath}:`, config.schema.join(', '));
767
+ }
768
+ }
769
+ }
770
+ }
771
+ } catch (idxError) {
772
+ // Index file doesn't exist or is corrupted, rebuild from data
773
+ if (this.opts.debugMode) {
774
+ console.log('📂 No index file found, rebuilding indexes from data');
775
+ }
776
+ // We can't rebuild index without violating no-memory-storage rule
777
+ // Index will be rebuilt as needed during queries
778
+ }
779
+ } else {
780
+ // No indexed fields, no need to rebuild indexes
781
+ }
782
+ this.performanceStats.loads++;
783
+ this.performanceStats.loadTime += Date.now() - startTime;
784
+ this.emit('loaded', this.writeBuffer.length);
785
+ } catch (error) {
786
+ console.error('Failed to load database:', error);
787
+ throw error;
788
+ } finally {
789
+ this.isLoading = false;
790
+ }
791
+ }
792
+
793
+ /**
794
+ * Save data to file
795
+ * @param {boolean} inQueue - Whether to execute within the operation queue (default: false)
796
+ */
797
+ async save(inQueue = false) {
798
+ this._validateInitialization('save');
799
+ if (this.opts.debugMode) {
800
+ console.log(`💾 save() called: writeBuffer.length=${this.writeBuffer.length}, offsets.length=${this.offsets.length}`);
801
+ }
802
+
803
+ // Auto-save removed - no need to pause anything
804
+
805
+ try {
806
+ // CRITICAL FIX: Wait for any ongoing save operations to complete
807
+ if (this.isSaving) {
808
+ if (this.opts.debugMode) {
809
+ console.log('💾 save(): waiting for previous save to complete');
810
+ }
811
+ // Wait for previous save to complete
812
+ while (this.isSaving) {
813
+ await new Promise(resolve => setTimeout(resolve, 10));
814
+ }
815
+
816
+ // Check if data changed since the previous save completed
817
+ const hasDataToSave = this.writeBuffer.length > 0 || this.deletedIds.size > 0;
818
+ const needsStructureCreation = this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0;
819
+ if (!hasDataToSave && !needsStructureCreation) {
820
+ if (this.opts.debugMode) {
821
+ console.log('💾 Save: No new data to save since previous save completed');
822
+ }
823
+ return; // Nothing new to save
824
+ }
825
+ }
826
+
827
+ // CRITICAL FIX: Check if there's actually data to save before proceeding
828
+ // But allow save if we need to create database structure (index files, etc.)
829
+ const hasDataToSave = this.writeBuffer.length > 0 || this.deletedIds.size > 0;
830
+ const needsStructureCreation = this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0;
831
+ if (!hasDataToSave && !needsStructureCreation) {
832
+ if (this.opts.debugMode) {
833
+ console.log('💾 Save: No data to save (writeBuffer empty and no deleted records)');
834
+ }
835
+ return; // Nothing to save
836
+ }
837
+ if (inQueue) {
838
+ if (this.opts.debugMode) {
839
+ console.log(`💾 save(): executing in queue`);
840
+ }
841
+ return this.operationQueue.enqueue(async () => {
842
+ return this._doSave();
843
+ });
844
+ } else {
845
+ if (this.opts.debugMode) {
846
+ console.log(`💾 save(): calling _doSave() directly`);
847
+ }
848
+ return this._doSave();
849
+ }
850
+ } finally {
851
+ // Auto-save removed - no need to resume anything
852
+ }
853
+ }
854
+
855
+ /**
856
+ * Internal save implementation (without queue)
857
+ */
858
+ async _doSave() {
859
+ // CRITICAL FIX: Check if database is destroyed
860
+ if (this.destroyed) return;
861
+
862
+ // CRITICAL FIX: Use atomic check-and-set to prevent concurrent save operations
863
+ if (this.isSaving) {
864
+ if (this.opts.debugMode) {
865
+ console.log('💾 _doSave: Save operation already in progress, skipping');
866
+ }
867
+ return;
868
+ }
869
+
870
+ // CRITICAL FIX: Check if there's actually data to save or structure to create
871
+ const hasDataToSave = this.writeBuffer.length > 0 || this.deletedIds.size > 0;
872
+ const needsStructureCreation = this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0;
873
+ if (!hasDataToSave && !needsStructureCreation) {
874
+ if (this.opts.debugMode) {
875
+ console.log('💾 _doSave: No data to save (writeBuffer empty and no deleted records)');
876
+ }
877
+ return; // Nothing to save
878
+ }
879
+
880
+ // CRITICAL FIX: Set saving flag immediately to prevent race conditions
881
+ this.isSaving = true;
882
+ try {
883
+ const startTime = Date.now();
884
+
885
+ // CRITICAL FIX: Ensure file path is valid
886
+ this.ensureFilePath();
887
+
888
+ // CRITICAL FIX: Wait for ALL pending operations to complete before save
889
+ await this._waitForPendingOperations();
890
+
891
+ // CRITICAL FIX: Capture writeBuffer and deletedIds at the start to prevent race conditions
892
+ const writeBufferSnapshot = [...this.writeBuffer];
893
+ const deletedIdsSnapshot = new Set(this.deletedIds);
894
+
895
+ // OPTIMIZATION: Process pending index updates in batch before save
896
+ if (this.pendingIndexUpdates && this.pendingIndexUpdates.length > 0) {
897
+ if (this.opts.debugMode) {
898
+ console.log(`💾 Save: Processing ${this.pendingIndexUpdates.length} pending index updates in batch`);
899
+ }
900
+
901
+ // Extract records and line numbers for batch processing
902
+ const records = this.pendingIndexUpdates.map(update => update.record);
903
+ const startLineNumber = this.pendingIndexUpdates[0].lineNumber;
904
+
905
+ // Process index updates in batch
906
+ await this.indexManager.addBatch(records, startLineNumber);
907
+
908
+ // Clear pending updates
909
+ this.pendingIndexUpdates = [];
910
+ }
911
+
912
+ // CRITICAL FIX: Flush write buffer completely after capturing snapshot
913
+ await this._flushWriteBufferCompletely();
914
+
915
+ // CRITICAL FIX: Wait for all I/O operations to complete before clearing writeBuffer
916
+ await this._waitForIOCompletion();
917
+
918
+ // CRITICAL FIX: Verify write buffer is empty after I/O completion
919
+ // But allow for ongoing insertions during high-volume scenarios
920
+ if (this.writeBuffer.length > 0) {
921
+ if (this.opts.debugMode) {
922
+ console.log(`💾 Save: WriteBuffer still has ${this.writeBuffer.length} items after flush - this may indicate ongoing insertions`);
923
+ }
924
+
925
+ // If we have a reasonable number of items, continue processing
926
+ if (this.writeBuffer.length < 10000) {
927
+ // Reasonable threshold
928
+ if (this.opts.debugMode) {
929
+ console.log(`💾 Save: Continuing to process remaining ${this.writeBuffer.length} items`);
930
+ }
931
+ // Continue with the save process - the remaining items will be included in the final save
932
+ } else {
933
+ // Too many items remaining - likely a real problem
934
+ throw new Error(`WriteBuffer has too many items after flush: ${this.writeBuffer.length} items remaining (threshold: 10000)`);
935
+ }
936
+ }
937
+
938
+ // OPTIMIZATION: Parallel operations - cleanup and data preparation
939
+ let allData = [];
940
+ let orphanedCount = 0;
941
+
942
+ // Check if there are new records to save (after flush, writeBuffer should be empty)
943
+ if (this.opts.debugMode) {
944
+ console.log(`💾 Save: writeBuffer.length=${this.writeBuffer.length}, writeBufferSnapshot.length=${writeBufferSnapshot.length}`);
945
+ }
946
+ if (this.writeBuffer.length > 0) {
947
+ if (this.opts.debugMode) {
948
+ console.log(`💾 Save: WriteBuffer has ${writeBufferSnapshot.length} records, using streaming approach`);
949
+ }
950
+
951
+ // Note: processTermMapping is already called during insert/update operations
952
+ // No need to call it again here to avoid double processing
953
+
954
+ // OPTIMIZATION: Check if we can skip reading existing records
955
+ // Only use streaming if we have existing records AND we're not just appending new records
956
+ const hasExistingRecords = this.indexOffset > 0 && this.offsets.length > 0 && writeBufferSnapshot.length > 0;
957
+ if (!hasExistingRecords && deletedIdsSnapshot.size === 0) {
958
+ // OPTIMIZATION: No existing records to read, just use writeBuffer
959
+ allData = [...writeBufferSnapshot];
960
+ } else {
961
+ // OPTIMIZATION: Parallel operations - cleanup and streaming
962
+ const parallelOperations = [];
963
+
964
+ // Add term cleanup if enabled
965
+ if (this.opts.termMappingCleanup && this.termManager) {
966
+ parallelOperations.push(Promise.resolve().then(() => {
967
+ orphanedCount = this.termManager.cleanupOrphanedTerms();
968
+ if (this.opts.debugMode && orphanedCount > 0) {
969
+ console.log(`🧹 Cleaned up ${orphanedCount} orphaned terms`);
970
+ }
971
+ }));
972
+ }
973
+
974
+ // Add streaming operation
975
+ parallelOperations.push(this._streamExistingRecords(deletedIdsSnapshot, writeBufferSnapshot).then(existingRecords => {
976
+ allData = [...existingRecords];
977
+
978
+ // OPTIMIZATION: Use Map for faster lookups
979
+ const existingRecordMap = new Map(existingRecords.filter(r => r && r.id).map(r => [r.id, r]));
980
+ for (const record of writeBufferSnapshot) {
981
+ if (!deletedIdsSnapshot.has(record.id)) {
982
+ if (existingRecordMap.has(record.id)) {
983
+ // Replace existing record
984
+ const existingIndex = allData.findIndex(r => r.id === record.id);
985
+ allData[existingIndex] = record;
986
+ } else {
987
+ // Add new record
988
+ allData.push(record);
989
+ }
990
+ }
991
+ }
992
+ }));
993
+
994
+ // Execute parallel operations
995
+ await Promise.all(parallelOperations);
996
+ }
997
+ } else {
998
+ // CRITICAL FIX: When writeBuffer is empty, use streaming approach for existing records
999
+ if (this.opts.debugMode) {
1000
+ console.log(`💾 Save: Checking streaming condition: indexOffset=${this.indexOffset}, deletedIds.size=${this.deletedIds.size}`);
1001
+ console.log(`💾 Save: writeBuffer.length=${this.writeBuffer.length}`);
1002
+ }
1003
+ if (this.indexOffset > 0 || this.deletedIds.size > 0) {
1004
+ try {
1005
+ if (this.opts.debugMode) {
1006
+ console.log(`💾 Save: Using streaming approach for existing records`);
1007
+ console.log(`💾 Save: indexOffset: ${this.indexOffset}, offsets.length: ${this.offsets.length}`);
1008
+ console.log(`💾 Save: deletedIds to filter:`, Array.from(deletedIdsSnapshot));
1009
+ }
1010
+
1011
+ // OPTIMIZATION: Parallel operations - cleanup and streaming
1012
+ const parallelOperations = [];
1013
+
1014
+ // Add term cleanup if enabled
1015
+ if (this.opts.termMappingCleanup && this.termManager) {
1016
+ parallelOperations.push(Promise.resolve().then(() => {
1017
+ orphanedCount = this.termManager.cleanupOrphanedTerms();
1018
+ if (this.opts.debugMode && orphanedCount > 0) {
1019
+ console.log(`🧹 Cleaned up ${orphanedCount} orphaned terms`);
1020
+ }
1021
+ }));
1022
+ }
1023
+
1024
+ // Add streaming operation
1025
+ parallelOperations.push(this._streamExistingRecords(deletedIdsSnapshot, writeBufferSnapshot).then(existingRecords => {
1026
+ if (this.opts.debugMode) {
1027
+ console.log(`💾 Save: _streamExistingRecords returned ${existingRecords.length} records`);
1028
+ console.log(`💾 Save: existingRecords:`, existingRecords);
1029
+ }
1030
+ // Combine existing records with new records from writeBuffer
1031
+ allData = [...existingRecords, ...writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(record.id))];
1032
+ }).catch(error => {
1033
+ if (this.opts.debugMode) {
1034
+ console.log(`💾 Save: _streamExistingRecords failed:`, error.message);
1035
+ }
1036
+ // CRITICAL FIX: Use safe fallback to preserve existing data instead of losing it
1037
+ return this._loadExistingRecordsFallback(deletedIdsSnapshot, writeBufferSnapshot).then(fallbackRecords => {
1038
+ allData = [...fallbackRecords, ...writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(record.id))];
1039
+ if (this.opts.debugMode) {
1040
+ console.log(`💾 Save: Fallback preserved ${fallbackRecords.length} existing records, total: ${allData.length}`);
1041
+ }
1042
+ }).catch(fallbackError => {
1043
+ if (this.opts.debugMode) {
1044
+ console.log(`💾 Save: All fallback methods failed:`, fallbackError.message);
1045
+ console.log(`💾 Save: CRITICAL - Data loss may occur, only writeBuffer will be saved`);
1046
+ }
1047
+ // Last resort: at least save what we have in writeBuffer
1048
+ allData = writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(record.id));
1049
+ });
1050
+ }));
1051
+
1052
+ // Execute parallel operations
1053
+ await Promise.all(parallelOperations);
1054
+ } catch (error) {
1055
+ if (this.opts.debugMode) {
1056
+ console.log(`💾 Save: Streaming approach failed, falling back to writeBuffer only: ${error.message}`);
1057
+ }
1058
+ // CRITICAL FIX: Use safe fallback to preserve existing data instead of losing it
1059
+ try {
1060
+ const fallbackRecords = await this._loadExistingRecordsFallback(deletedIdsSnapshot, writeBufferSnapshot);
1061
+ allData = [...fallbackRecords, ...writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(record.id))];
1062
+ if (this.opts.debugMode) {
1063
+ console.log(`💾 Save: Fallback preserved ${fallbackRecords.length} existing records, total: ${allData.length}`);
1064
+ }
1065
+ } catch (fallbackError) {
1066
+ if (this.opts.debugMode) {
1067
+ console.log(`💾 Save: All fallback methods failed:`, fallbackError.message);
1068
+ console.log(`💾 Save: CRITICAL - Data loss may occur, only writeBuffer will be saved`);
1069
+ }
1070
+ // Last resort: at least save what we have in writeBuffer
1071
+ allData = writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(record.id));
1072
+ }
1073
+ }
1074
+ } else {
1075
+ // No existing data, use only writeBuffer
1076
+ allData = writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(record.id));
1077
+ }
1078
+ }
1079
+
1080
+ // CRITICAL FIX: Calculate offsets based on actual serialized data that will be written
1081
+ // This ensures consistency between offset calculation and file writing
1082
+ const jsonlData = allData.length > 0 ? this.serializer.serializeBatch(allData) : '';
1083
+ const jsonlString = jsonlData.toString('utf8');
1084
+ const lines = jsonlString.split('\n').filter(line => line.trim());
1085
+ this.offsets = [];
1086
+ let currentOffset = 0;
1087
+ for (let i = 0; i < lines.length; i++) {
1088
+ this.offsets.push(currentOffset);
1089
+ // CRITICAL FIX: Use actual line length including newline for accurate offset calculation
1090
+ // This accounts for UTF-8 encoding differences (e.g., 'ação' vs 'acao')
1091
+ const lineWithNewline = lines[i] + '\n';
1092
+ currentOffset += Buffer.byteLength(lineWithNewline, 'utf8');
1093
+ }
1094
+
1095
+ // CRITICAL FIX: Ensure indexOffset matches actual file size
1096
+ this.indexOffset = currentOffset;
1097
+ if (this.opts.debugMode) {
1098
+ console.log(`💾 Save: Calculated indexOffset: ${this.indexOffset}, allData.length: ${allData.length}`);
1099
+ }
1100
+
1101
+ // OPTIMIZATION: Parallel operations - file writing and index data preparation
1102
+ const parallelWriteOperations = [];
1103
+
1104
+ // Add main file write operation
1105
+ parallelWriteOperations.push(this.fileHandler.writeBatch([jsonlData]));
1106
+
1107
+ // Add index file operations - ALWAYS save offsets, even without indexed fields
1108
+ if (this.indexManager) {
1109
+ const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
1110
+
1111
+ // OPTIMIZATION: Parallel data preparation
1112
+ const indexDataPromise = Promise.resolve({
1113
+ index: this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0 ? this.indexManager.toJSON() : {},
1114
+ offsets: this.offsets,
1115
+ // Save actual offsets for efficient file operations
1116
+ indexOffset: this.indexOffset // Save file size for proper range calculations
1117
+ });
1118
+
1119
+ // Add term mapping data if needed
1120
+ const termMappingFields = this.getTermMappingFields();
1121
+ if (termMappingFields.length > 0 && this.termManager) {
1122
+ const termDataPromise = this.termManager.saveTerms();
1123
+
1124
+ // Combine index data and term data
1125
+ const combinedDataPromise = Promise.all([indexDataPromise, termDataPromise]).then(([indexData, termData]) => {
1126
+ indexData.termMapping = termData;
1127
+ return indexData;
1128
+ });
1129
+
1130
+ // Add index file write operation
1131
+ parallelWriteOperations.push(combinedDataPromise.then(indexData => {
1132
+ const idxFileHandler = new _FileHandler.default(idxPath, this.fileMutex, this.opts);
1133
+ return idxFileHandler.writeAll(JSON.stringify(indexData, null, 2));
1134
+ }));
1135
+ } else {
1136
+ // Add index file write operation without term mapping
1137
+ parallelWriteOperations.push(indexDataPromise.then(indexData => {
1138
+ const idxFileHandler = new _FileHandler.default(idxPath, this.fileMutex, this.opts);
1139
+ return idxFileHandler.writeAll(JSON.stringify(indexData, null, 2));
1140
+ }));
1141
+ }
1142
+ }
1143
+
1144
+ // Execute parallel write operations
1145
+ await Promise.all(parallelWriteOperations);
1146
+ if (this.opts.debugMode) {
1147
+ console.log(`💾 Saved ${allData.length} records to ${this.normalizedFile}`);
1148
+ }
1149
+
1150
+ // CRITICAL FIX: Invalidate file size cache after save operation
1151
+ this._cachedFileStats = null;
1152
+ this.shouldSave = false;
1153
+ this.lastSaveTime = Date.now();
1154
+
1155
+ // Clear writeBuffer and deletedIds after successful save only if we had data to save
1156
+ if (allData.length > 0) {
1157
+ // Rebuild index when records were deleted to maintain consistency
1158
+ const hadDeletedRecords = deletedIdsSnapshot.size > 0;
1159
+ if (this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
1160
+ if (hadDeletedRecords) {
1161
+ // Clear the index and rebuild it from the remaining records
1162
+ this.indexManager.clear();
1163
+ if (this.opts.debugMode) {
1164
+ console.log(`🧹 Rebuilding index after removing ${deletedIdsSnapshot.size} deleted records`);
1165
+ }
1166
+
1167
+ // Rebuild index from the saved records
1168
+ for (let i = 0; i < allData.length; i++) {
1169
+ const record = allData[i];
1170
+ await this.indexManager.add(record, i);
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ // CRITICAL FIX: Clear all records that were in the snapshot
1176
+ // Use a more robust comparison that handles different data types
1177
+ const originalLength = this.writeBuffer.length;
1178
+ this.writeBuffer = this.writeBuffer.filter(record => {
1179
+ // For objects with id, compare by id
1180
+ if (typeof record === 'object' && record !== null && record.id) {
1181
+ return !writeBufferSnapshot.some(snapshotRecord => typeof snapshotRecord === 'object' && snapshotRecord !== null && snapshotRecord.id && snapshotRecord.id === record.id);
1182
+ }
1183
+ // For other types (Buffers, primitives), use strict equality
1184
+ return !writeBufferSnapshot.some(snapshotRecord => snapshotRecord === record);
1185
+ });
1186
+
1187
+ // Remove only the deleted IDs that were in the snapshot
1188
+ for (const deletedId of deletedIdsSnapshot) {
1189
+ this.deletedIds.delete(deletedId);
1190
+ }
1191
+
1192
+ // CRITICAL FIX: Ensure writeBuffer is completely cleared after successful save
1193
+ if (this.writeBuffer.length > 0) {
1194
+ if (this.opts.debugMode) {
1195
+ console.log(`💾 Save: Force clearing remaining ${this.writeBuffer.length} items from writeBuffer`);
1196
+ }
1197
+ // If there are still items in writeBuffer after filtering, clear them
1198
+ // This prevents the "writeBuffer has records" bug in destroy()
1199
+ this.writeBuffer = [];
1200
+ this.writeBufferOffsets = [];
1201
+ this.writeBufferSizes = [];
1202
+ }
1203
+
1204
+ // indexOffset already set correctly to currentOffset (total file size) above
1205
+ // No need to override it with record count
1206
+ }
1207
+
1208
+ // CRITICAL FIX: Always save index data to file after saving records
1209
+ await this._saveIndexDataToFile();
1210
+ this.performanceStats.saves++;
1211
+ this.performanceStats.saveTime += Date.now() - startTime;
1212
+ this.emit('saved', this.writeBuffer.length);
1213
+ } catch (error) {
1214
+ console.error('Failed to save database:', error);
1215
+ throw error;
1216
+ } finally {
1217
+ this.isSaving = false;
1218
+ }
1219
+ }
1220
+
1221
+ /**
1222
+ * Process term mapping for a record
1223
+ * @param {Object} record - Record to process
1224
+ * @param {boolean} isUpdate - Whether this is an update operation
1225
+ * @param {Object} oldRecord - Original record (for updates)
1226
+ */
1227
+ processTermMapping(record, isUpdate = false, oldRecord = null) {
1228
+ const termMappingFields = this.getTermMappingFields();
1229
+ if (!this.termManager || termMappingFields.length === 0) {
1230
+ return;
1231
+ }
1232
+
1233
+ // CRITICAL FIX: Don't modify the original record object
1234
+ // The record should already be a copy created in insert/update methods
1235
+ // This prevents reference modification issues
1236
+
1237
+ // Process each term mapping field
1238
+ for (const field of termMappingFields) {
1239
+ if (record[field] && Array.isArray(record[field])) {
1240
+ // Decrement old terms if this is an update
1241
+ if (isUpdate && oldRecord) {
1242
+ // Check if oldRecord has term IDs or terms
1243
+ const termIdsField = `${field}Ids`;
1244
+ if (oldRecord[termIdsField] && Array.isArray(oldRecord[termIdsField])) {
1245
+ // Use term IDs directly for decrementing
1246
+ for (const termId of oldRecord[termIdsField]) {
1247
+ this.termManager.decrementTermCount(termId);
1248
+ }
1249
+ } else if (oldRecord[field] && Array.isArray(oldRecord[field])) {
1250
+ // Use terms to decrement (fallback for backward compatibility)
1251
+ for (const term of oldRecord[field]) {
1252
+ const termId = this.termManager.termToId.get(term);
1253
+ if (termId) {
1254
+ this.termManager.decrementTermCount(termId);
1255
+ }
1256
+ }
1257
+ }
1258
+ }
1259
+
1260
+ // Clear old term IDs if this is an update
1261
+ if (isUpdate) {
1262
+ delete record[`${field}Ids`];
1263
+ }
1264
+
1265
+ // Process new terms - getTermId already increments the count
1266
+ const termIds = [];
1267
+ for (const term of record[field]) {
1268
+ const termId = this.termManager.getTermId(term);
1269
+ termIds.push(termId);
1270
+ }
1271
+ // Store term IDs in the record (for internal use)
1272
+ record[`${field}Ids`] = termIds;
1273
+ }
1274
+ }
1275
+ }
1276
+
1277
+ /**
1278
+ * Convert terms to term IDs for serialization (SPACE OPTIMIZATION)
1279
+ * @param {Object} record - Record to process
1280
+ * @returns {Object} - Record with terms converted to term IDs
1281
+ */
1282
+ removeTermIdsForSerialization(record) {
1283
+ const termMappingFields = this.getTermMappingFields();
1284
+ if (termMappingFields.length === 0 || !this.termManager) {
1285
+ return record;
1286
+ }
1287
+
1288
+ // Create a copy and convert terms to term IDs
1289
+ const optimizedRecord = {
1290
+ ...record
1291
+ };
1292
+ for (const field of termMappingFields) {
1293
+ if (optimizedRecord[field] && Array.isArray(optimizedRecord[field])) {
1294
+ // CRITICAL FIX: Only convert if values are strings (terms), skip if already numbers (term IDs)
1295
+ const firstValue = optimizedRecord[field][0];
1296
+ if (typeof firstValue === 'string') {
1297
+ // Convert terms to term IDs for storage
1298
+ optimizedRecord[field] = optimizedRecord[field].map(term => this.termManager.getTermIdWithoutIncrement(term));
1299
+ }
1300
+ // If already numbers (term IDs), leave as-is
1301
+ }
1302
+ }
1303
+ return optimizedRecord;
1304
+ }
1305
+
1306
+ /**
1307
+ * Convert term IDs back to terms after deserialization (SPACE OPTIMIZATION)
1308
+ * @param {Object} record - Record with term IDs
1309
+ * @returns {Object} - Record with terms restored
1310
+ */
1311
+ restoreTermIdsAfterDeserialization(record) {
1312
+ const termMappingFields = this.getTermMappingFields();
1313
+ if (termMappingFields.length === 0 || !this.termManager) {
1314
+ return record;
1315
+ }
1316
+
1317
+ // Create a copy and convert term IDs back to terms
1318
+ const restoredRecord = {
1319
+ ...record
1320
+ };
1321
+ for (const field of termMappingFields) {
1322
+ if (restoredRecord[field] && Array.isArray(restoredRecord[field])) {
1323
+ // Convert term IDs back to terms for user
1324
+ restoredRecord[field] = restoredRecord[field].map(termId => {
1325
+ const term = this.termManager.idToTerm.get(termId) || termId;
1326
+ return term;
1327
+ });
1328
+ }
1329
+
1330
+ // Remove the *Ids field that was added during serialization
1331
+ const idsFieldName = field + 'Ids';
1332
+ if (restoredRecord[idsFieldName]) {
1333
+ delete restoredRecord[idsFieldName];
1334
+ }
1335
+ }
1336
+ return restoredRecord;
1337
+ }
1338
+
1339
+ /**
1340
+ * Remove term mapping for a record
1341
+ * @param {Object} record - Record to process
1342
+ */
1343
+ removeTermMapping(record) {
1344
+ const termMappingFields = this.getTermMappingFields();
1345
+ if (!this.termManager || termMappingFields.length === 0) {
1346
+ return;
1347
+ }
1348
+
1349
+ // Process each term mapping field
1350
+ for (const field of termMappingFields) {
1351
+ // Use terms to decrement (term IDs are not stored in records anymore)
1352
+ if (record[field] && Array.isArray(record[field])) {
1353
+ for (const term of record[field]) {
1354
+ const termId = this.termManager.termToId.get(term);
1355
+ if (termId) {
1356
+ this.termManager.decrementTermCount(termId);
1357
+ }
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+
1363
+ /**
1364
+ * Process term mapping for multiple records in batch (OPTIMIZATION)
1365
+ * @param {Array} records - Records to process
1366
+ * @returns {Array} - Processed records with term mappings
1367
+ */
1368
+ processTermMappingBatch(records) {
1369
+ const termMappingFields = this.getTermMappingFields();
1370
+ if (!this.termManager || termMappingFields.length === 0 || !records.length) {
1371
+ return records;
1372
+ }
1373
+
1374
+ // OPTIMIZATION: Pre-collect all unique terms to minimize Map operations
1375
+ const allTerms = new Set();
1376
+ const fieldTerms = new Map(); // field -> Set of terms
1377
+
1378
+ for (const field of termMappingFields) {
1379
+ fieldTerms.set(field, new Set());
1380
+ for (const record of records) {
1381
+ if (record[field] && Array.isArray(record[field])) {
1382
+ for (const term of record[field]) {
1383
+ allTerms.add(term);
1384
+ fieldTerms.get(field).add(term);
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+
1390
+ // OPTIMIZATION: Batch process all terms at once using bulk operations
1391
+ const termIdMap = new Map();
1392
+ if (this.termManager.bulkGetTermIds) {
1393
+ // Use bulk operation if available
1394
+ const allTermsArray = Array.from(allTerms);
1395
+ const termIds = this.termManager.bulkGetTermIds(allTermsArray);
1396
+ for (let i = 0; i < allTermsArray.length; i++) {
1397
+ termIdMap.set(allTermsArray[i], termIds[i]);
1398
+ }
1399
+ } else {
1400
+ // Fallback to individual operations
1401
+ for (const term of allTerms) {
1402
+ termIdMap.set(term, this.termManager.getTermId(term));
1403
+ }
1404
+ }
1405
+
1406
+ // OPTIMIZATION: Process records using pre-computed term IDs
1407
+ return records.map(record => {
1408
+ const processedRecord = {
1409
+ ...record
1410
+ };
1411
+ for (const field of termMappingFields) {
1412
+ if (record[field] && Array.isArray(record[field])) {
1413
+ const termIds = record[field].map(term => termIdMap.get(term));
1414
+ processedRecord[`${field}Ids`] = termIds;
1415
+ }
1416
+ }
1417
+ return processedRecord;
1418
+ });
1419
+ }
1420
+
1421
+ /**
1422
+ * Calculate total size of serialized records (OPTIMIZATION)
1423
+ * @param {Array} records - Records to calculate size for
1424
+ * @returns {number} - Total size in bytes
1425
+ */
1426
+ calculateBatchSize(records) {
1427
+ if (!records || !records.length) return 0;
1428
+ let totalSize = 0;
1429
+ for (const record of records) {
1430
+ // OPTIMIZATION: Calculate size without creating the actual string
1431
+ // SPACE OPTIMIZATION: Remove term IDs before size calculation
1432
+ const cleanRecord = this.removeTermIdsForSerialization(record);
1433
+ const jsonString = this.serializer.serialize(cleanRecord).toString('utf8');
1434
+ totalSize += Buffer.byteLength(jsonString, 'utf8') + 1; // +1 for newline
1435
+ }
1436
+ return totalSize;
1437
+ }
1438
+
1439
+ /**
1440
+ * Begin an insert session for batch operations
1441
+ * @param {Object} sessionOptions - Options for the insert session
1442
+ * @returns {InsertSession} - The insert session instance
1443
+ */
1444
+ beginInsertSession(sessionOptions = {}) {
1445
+ if (this.destroyed) {
1446
+ throw new Error('Database is destroyed');
1447
+ }
1448
+ if (this.closed) {
1449
+ throw new Error('Database is closed. Call init() to reopen it.');
1450
+ }
1451
+ return new InsertSession(this, sessionOptions);
1452
+ }
1453
+
1454
+ /**
1455
+ * Insert a new record
1456
+ */
1457
+ async insert(data) {
1458
+ this._validateInitialization('insert');
1459
+ return this.operationQueue.enqueue(async () => {
1460
+ this.isInsideOperationQueue = true;
1461
+ try {
1462
+ // CRITICAL FIX: Validate state before insert operation
1463
+ this.validateState();
1464
+ if (!data || typeof data !== 'object') {
1465
+ throw new Error('Data must be an object');
1466
+ }
1467
+
1468
+ // CRITICAL FIX: Check abort signal before operation, but allow during destroy cleanup
1469
+ if (this.abortController.signal.aborted && !this.destroying) {
1470
+ throw new Error('Database is destroyed');
1471
+ }
1472
+
1473
+ // Initialize schema if not already done (auto-detect from first record)
1474
+ if (this.serializer && !this.serializer.schemaManager.isInitialized) {
1475
+ this.serializer.initializeSchema(data, true);
1476
+ if (this.opts.debugMode) {
1477
+ console.log(`🔍 Schema auto-detected from first insert: ${this.serializer.getSchema().join(', ')} [${this.instanceId}]`);
1478
+ }
1479
+ }
1480
+
1481
+ // OPTIMIZATION: Process single insert with deferred index updates
1482
+ // CRITICAL FIX: Clone the object to prevent reference modification
1483
+ const clonedData = {
1484
+ ...data
1485
+ };
1486
+ const id = clonedData.id || this.generateId();
1487
+ const record = {
1488
+ ...data,
1489
+ id
1490
+ };
1491
+
1492
+ // OPTIMIZATION: Process term mapping
1493
+ this.processTermMapping(record);
1494
+ if (this.opts.debugMode) {
1495
+ // console.log(`💾 insert(): writeBuffer(before)=${this.writeBuffer.length}`)
1496
+ }
1497
+
1498
+ // Apply schema enforcement - convert to array format and back to enforce schema
1499
+ const schemaEnforcedRecord = this.applySchemaEnforcement(record);
1500
+
1501
+ // Don't store in this.data - only use writeBuffer and index
1502
+ this.writeBuffer.push(schemaEnforcedRecord);
1503
+ if (this.opts.debugMode) {
1504
+ console.log(`🔍 INSERT: Added record to writeBuffer, length now: ${this.writeBuffer.length}`);
1505
+ }
1506
+
1507
+ // OPTIMIZATION: Calculate and store offset and size for writeBuffer record
1508
+ // SPACE OPTIMIZATION: Remove term IDs before serialization
1509
+ const cleanRecord = this.removeTermIdsForSerialization(record);
1510
+ const recordJson = this.serializer.serialize(cleanRecord).toString('utf8');
1511
+ const recordSize = Buffer.byteLength(recordJson, 'utf8');
1512
+
1513
+ // Calculate offset based on end of file + previous writeBuffer sizes
1514
+ const previousWriteBufferSize = this.writeBufferSizes.reduce((sum, size) => sum + size, 0);
1515
+ const recordOffset = this.indexOffset + previousWriteBufferSize;
1516
+ this.writeBufferOffsets.push(recordOffset);
1517
+ this.writeBufferSizes.push(recordSize);
1518
+
1519
+ // OPTIMIZATION: Use the current writeBuffer size as the line number (0-based index)
1520
+ const lineNumber = this.writeBuffer.length - 1;
1521
+
1522
+ // OPTIMIZATION: Defer index updates to batch processing
1523
+ // Store the record for batch index processing
1524
+ if (!this.pendingIndexUpdates) {
1525
+ this.pendingIndexUpdates = [];
1526
+ }
1527
+ this.pendingIndexUpdates.push({
1528
+ record,
1529
+ lineNumber
1530
+ });
1531
+
1532
+ // Manual save is now the responsibility of the application
1533
+ this.shouldSave = true;
1534
+ this.performanceStats.operations++;
1535
+
1536
+ // Auto-save manager removed - manual save required
1537
+
1538
+ this.emit('inserted', record);
1539
+ return record;
1540
+ } finally {
1541
+ this.isInsideOperationQueue = false;
1542
+ }
1543
+ });
1544
+ }
1545
+
1546
+ /**
1547
+ * Insert multiple records in batch (OPTIMIZATION)
1548
+ */
1549
+ async insertBatch(dataArray) {
1550
+ this._validateInitialization('insertBatch');
1551
+
1552
+ // If we're already inside the operation queue (e.g., from insert()), avoid re-enqueueing to prevent deadlocks
1553
+ if (this.isInsideOperationQueue) {
1554
+ if (this.opts.debugMode) {
1555
+ console.log(`💾 insertBatch inline: insideQueue=${this.isInsideOperationQueue}, size=${Array.isArray(dataArray) ? dataArray.length : 0}`);
1556
+ }
1557
+ return await this._insertBatchInternal(dataArray);
1558
+ }
1559
+ return this.operationQueue.enqueue(async () => {
1560
+ this.isInsideOperationQueue = true;
1561
+ try {
1562
+ if (this.opts.debugMode) {
1563
+ console.log(`💾 insertBatch enqueued: size=${Array.isArray(dataArray) ? dataArray.length : 0}`);
1564
+ }
1565
+ return await this._insertBatchInternal(dataArray);
1566
+ } finally {
1567
+ this.isInsideOperationQueue = false;
1568
+ }
1569
+ });
1570
+ }
1571
+
1572
+ /**
1573
+ * Internal implementation for insertBatch to allow inline execution when already inside the queue
1574
+ */
1575
+ async _insertBatchInternal(dataArray) {
1576
+ // CRITICAL FIX: Validate state before insert operation
1577
+ this.validateState();
1578
+ if (!Array.isArray(dataArray) || dataArray.length === 0) {
1579
+ throw new Error('DataArray must be a non-empty array');
1580
+ }
1581
+
1582
+ // CRITICAL FIX: Check abort signal before operation, but allow during destroy cleanup
1583
+ if (this.abortController.signal.aborted && !this.destroying) {
1584
+ throw new Error('Database is destroyed');
1585
+ }
1586
+ if (this.opts.debugMode) {
1587
+ console.log(`💾 _insertBatchInternal: processing size=${dataArray.length}, startWriteBuffer=${this.writeBuffer.length}`);
1588
+ }
1589
+ const records = [];
1590
+ const startLineNumber = this.writeBuffer.length;
1591
+
1592
+ // Initialize schema if not already done (auto-detect from first record)
1593
+ if (this.serializer && !this.serializer.schemaManager.isInitialized && dataArray.length > 0) {
1594
+ this.serializer.initializeSchema(dataArray[0], true);
1595
+ if (this.opts.debugMode) {
1596
+ console.log(`🔍 Schema auto-detected from first batch insert: ${this.serializer.getSchema().join(', ')} [${this.instanceId}]`);
1597
+ }
1598
+ }
1599
+
1600
+ // OPTIMIZATION: Process all records in batch
1601
+ for (let i = 0; i < dataArray.length; i++) {
1602
+ const data = dataArray[i];
1603
+ if (!data || typeof data !== 'object') {
1604
+ throw new Error(`Data at index ${i} must be an object`);
1605
+ }
1606
+ const id = data.id || this.generateId();
1607
+ const record = {
1608
+ ...data,
1609
+ id
1610
+ };
1611
+ records.push(record);
1612
+ }
1613
+
1614
+ // OPTIMIZATION: Batch process term mapping
1615
+ const processedRecords = this.processTermMappingBatch(records);
1616
+
1617
+ // Apply schema enforcement to all records
1618
+ const schemaEnforcedRecords = processedRecords.map(record => this.applySchemaEnforcement(record));
1619
+
1620
+ // OPTIMIZATION: Add all records to writeBuffer at once
1621
+ this.writeBuffer.push(...schemaEnforcedRecords);
1622
+
1623
+ // OPTIMIZATION: Calculate offsets and sizes in batch (O(n))
1624
+ let runningTotalSize = this.writeBufferSizes.reduce((sum, size) => sum + size, 0);
1625
+ for (let i = 0; i < processedRecords.length; i++) {
1626
+ const record = processedRecords[i];
1627
+ // SPACE OPTIMIZATION: Remove term IDs before serialization
1628
+ const cleanRecord = this.removeTermIdsForSerialization(record);
1629
+ const recordJson = this.serializer.serialize(cleanRecord).toString('utf8');
1630
+ const recordSize = Buffer.byteLength(recordJson, 'utf8');
1631
+ const recordOffset = this.indexOffset + runningTotalSize;
1632
+ runningTotalSize += recordSize;
1633
+ this.writeBufferOffsets.push(recordOffset);
1634
+ this.writeBufferSizes.push(recordSize);
1635
+ }
1636
+
1637
+ // OPTIMIZATION: Batch process index updates
1638
+ if (!this.pendingIndexUpdates) {
1639
+ this.pendingIndexUpdates = [];
1640
+ }
1641
+ for (let i = 0; i < processedRecords.length; i++) {
1642
+ const lineNumber = startLineNumber + i;
1643
+ this.pendingIndexUpdates.push({
1644
+ record: processedRecords[i],
1645
+ lineNumber
1646
+ });
1647
+ }
1648
+ this.shouldSave = true;
1649
+ this.performanceStats.operations += processedRecords.length;
1650
+
1651
+ // Emit events for all records
1652
+ if (this.listenerCount('inserted') > 0) {
1653
+ for (const record of processedRecords) {
1654
+ this.emit('inserted', record);
1655
+ }
1656
+ }
1657
+ if (this.opts.debugMode) {
1658
+ console.log(`💾 _insertBatchInternal: done. added=${processedRecords.length}, writeBuffer=${this.writeBuffer.length}`);
1659
+ }
1660
+ return processedRecords;
1661
+ }
1662
+
1663
+ /**
1664
+ * Find records matching criteria
1665
+ */
1666
+ async find(criteria = {}, options = {}) {
1667
+ this._validateInitialization('find');
1668
+
1669
+ // CRITICAL FIX: Validate state before find operation
1670
+ this.validateState();
1671
+
1672
+ // OPTIMIZATION: Find searches writeBuffer directly
1673
+
1674
+ const startTime = Date.now();
1675
+ if (this.opts.debugMode) {
1676
+ console.log(`🔍 FIND START: criteria=${JSON.stringify(criteria)}, writeBuffer=${this.writeBuffer.length}`);
1677
+ }
1678
+ try {
1679
+ // Validate indexed query mode if enabled
1680
+ if (this.opts.indexedQueryMode === 'strict') {
1681
+ this._validateIndexedQuery(criteria);
1682
+ }
1683
+
1684
+ // Get results from file (QueryManager already handles term ID restoration)
1685
+ const fileResultsWithTerms = await this.queryManager.find(criteria, options);
1686
+
1687
+ // Get results from writeBuffer
1688
+ const allPendingRecords = [...this.writeBuffer];
1689
+ const writeBufferResults = this.queryManager.matchesCriteria ? allPendingRecords.filter(record => this.queryManager.matchesCriteria(record, criteria, options)) : allPendingRecords;
1690
+
1691
+ // SPACE OPTIMIZATION: Restore term IDs to terms for writeBuffer results (unless disabled)
1692
+ const writeBufferResultsWithTerms = options.restoreTerms !== false ? writeBufferResults.map(record => this.restoreTermIdsAfterDeserialization(record)) : writeBufferResults;
1693
+
1694
+ // Combine results, removing duplicates (writeBuffer takes precedence)
1695
+ // OPTIMIZATION: Use parallel processing for better performance when writeBuffer has many records
1696
+ let allResults;
1697
+ if (writeBufferResults.length > 50) {
1698
+ // Parallel approach for large writeBuffer
1699
+ const [fileResultsSet, writeBufferSet] = await Promise.all([Promise.resolve(new Set(fileResultsWithTerms.map(r => r.id))), Promise.resolve(new Set(writeBufferResultsWithTerms.map(r => r.id)))]);
1700
+
1701
+ // Merge efficiently: keep file results not in writeBuffer, then add all writeBuffer results
1702
+ const filteredFileResults = await Promise.resolve(fileResultsWithTerms.filter(r => !writeBufferSet.has(r.id)));
1703
+ allResults = [...filteredFileResults, ...writeBufferResultsWithTerms];
1704
+ } else {
1705
+ // Sequential approach for small writeBuffer (original logic)
1706
+ allResults = [...fileResultsWithTerms];
1707
+
1708
+ // Replace file records with writeBuffer records and add new writeBuffer records
1709
+ for (const record of writeBufferResultsWithTerms) {
1710
+ const existingIndex = allResults.findIndex(r => r.id === record.id);
1711
+ if (existingIndex !== -1) {
1712
+ // Replace existing record with writeBuffer version
1713
+ allResults[existingIndex] = record;
1714
+ } else {
1715
+ // Add new record from writeBuffer
1716
+ allResults.push(record);
1717
+ }
1718
+ }
1719
+ }
1720
+
1721
+ // Remove records that are marked as deleted
1722
+ const finalResults = allResults.filter(record => !this.deletedIds.has(record.id));
1723
+ if (this.opts.debugMode) {
1724
+ console.log(`🔍 Database.find returning: ${finalResults?.length || 0} records (${fileResultsWithTerms.length} from file, ${writeBufferResults.length} from writeBuffer, ${this.deletedIds.size} deleted), type: ${typeof finalResults}, isArray: ${Array.isArray(finalResults)}`);
1725
+ }
1726
+ this.performanceStats.queryTime += Date.now() - startTime;
1727
+ return finalResults;
1728
+ } catch (error) {
1729
+ // Don't log expected errors in strict mode or for array field validation
1730
+ if (this.opts.indexedQueryMode !== 'strict' || !error.message.includes('Strict indexed mode')) {
1731
+ // Don't log errors for array field validation as they are expected
1732
+ if (!error.message.includes('Invalid query for array field')) {
1733
+ console.error('Query failed:', error);
1734
+ }
1735
+ }
1736
+ throw error;
1737
+ }
1738
+ }
1739
+
1740
+ /**
1741
+ * Validate indexed query mode for strict mode
1742
+ * @private
1743
+ */
1744
+ _validateIndexedQuery(criteria) {
1745
+ if (!criteria || typeof criteria !== 'object') {
1746
+ return; // Allow null/undefined criteria
1747
+ }
1748
+ const indexedFields = Object.keys(this.opts.indexes || {});
1749
+ if (indexedFields.length === 0) {
1750
+ return; // No indexed fields, allow all queries
1751
+ }
1752
+ const queryFields = this._extractQueryFields(criteria);
1753
+ const nonIndexedFields = queryFields.filter(field => !indexedFields.includes(field));
1754
+ if (nonIndexedFields.length > 0) {
1755
+ const availableFields = indexedFields.length > 0 ? indexedFields.join(', ') : 'none';
1756
+ if (nonIndexedFields.length === 1) {
1757
+ throw new Error(`Strict indexed mode: Field '${nonIndexedFields[0]}' is not indexed. Available indexed fields: ${availableFields}`);
1758
+ } else {
1759
+ throw new Error(`Strict indexed mode: Fields '${nonIndexedFields.join("', '")}' are not indexed. Available indexed fields: ${availableFields}`);
1760
+ }
1761
+ }
1762
+ }
1763
+
1764
+ /**
1765
+ * Create a shallow copy of a record for change detection
1766
+ * Optimized for known field types: number, string, null, or single-level arrays
1767
+ * @private
1768
+ */
1769
+ _createShallowCopy(record) {
1770
+ const copy = {};
1771
+ // Use for...in loop for better performance
1772
+ for (const key in record) {
1773
+ const value = record[key];
1774
+ // Optimize for common types first
1775
+ if (value === null || typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') {
1776
+ copy[key] = value;
1777
+ } else if (Array.isArray(value)) {
1778
+ // Only copy if array has elements and is not empty
1779
+ copy[key] = value.length > 0 ? value.slice() : [];
1780
+ } else if (typeof value === 'object') {
1781
+ // For complex objects, use shallow copy
1782
+ copy[key] = {
1783
+ ...value
1784
+ };
1785
+ } else {
1786
+ copy[key] = value;
1787
+ }
1788
+ }
1789
+ return copy;
1790
+ }
1791
+
1792
+ /**
1793
+ * Create an intuitive API wrapper using a class with Proxy
1794
+ * Combines the benefits of classes with the flexibility of Proxy
1795
+ * @private
1796
+ */
1797
+ _createEntryProxy(entry, originalRecord) {
1798
+ // Create a class instance that wraps the entry
1799
+ const iterateEntry = new IterateEntry(entry, originalRecord);
1800
+
1801
+ // Create a lightweight proxy that only intercepts property access
1802
+ return new Proxy(iterateEntry, {
1803
+ get(target, property) {
1804
+ // Handle special methods
1805
+ if (property === 'delete') {
1806
+ return () => target.delete();
1807
+ }
1808
+ if (property === 'value') {
1809
+ return target.value;
1810
+ }
1811
+ if (property === 'isModified') {
1812
+ return target.isModified;
1813
+ }
1814
+ if (property === 'isMarkedForDeletion') {
1815
+ return target.isMarkedForDeletion;
1816
+ }
1817
+
1818
+ // For all other properties, return from the underlying entry
1819
+ return target._entry[property];
1820
+ },
1821
+ set(target, property, value) {
1822
+ // Set the value in the underlying entry
1823
+ target._entry[property] = value;
1824
+ target._modified = true;
1825
+ return true;
1826
+ }
1827
+ });
1828
+ }
1829
+
1830
+ /**
1831
+ * Create a high-performance wrapper for maximum speed
1832
+ * @private
1833
+ */
1834
+ _createHighPerformanceWrapper(entry, originalRecord) {
1835
+ // Create a simple wrapper object for high performance
1836
+ const wrapper = {
1837
+ value: entry,
1838
+ delete: () => {
1839
+ entry._markedForDeletion = true;
1840
+ return true;
1841
+ }
1842
+ };
1843
+
1844
+ // Mark for change tracking
1845
+ entry._modified = false;
1846
+ entry._markedForDeletion = false;
1847
+ return wrapper;
1848
+ }
1849
+
1850
+ /**
1851
+ * Check if a record has changed using optimized comparison
1852
+ * Optimized for known field types: number, string, null, or single-level arrays
1853
+ * @private
1854
+ */
1855
+ _hasRecordChanged(current, original) {
1856
+ // Quick reference check first
1857
+ if (current === original) return false;
1858
+
1859
+ // Compare each field - optimized for common types
1860
+ for (const key in current) {
1861
+ const currentValue = current[key];
1862
+ const originalValue = original[key];
1863
+
1864
+ // Quick reference check (most common case)
1865
+ if (currentValue === originalValue) continue;
1866
+
1867
+ // Handle null values
1868
+ if (currentValue === null || originalValue === null) {
1869
+ if (currentValue !== originalValue) return true;
1870
+ continue;
1871
+ }
1872
+
1873
+ // Handle primitive types (number, string, boolean) - most common
1874
+ const currentType = typeof currentValue;
1875
+ if (currentType === 'number' || currentType === 'string' || currentType === 'boolean') {
1876
+ if (currentType !== typeof originalValue || currentValue !== originalValue) return true;
1877
+ continue;
1878
+ }
1879
+
1880
+ // Handle arrays (single-level) - second most common
1881
+ if (Array.isArray(currentValue)) {
1882
+ if (!Array.isArray(originalValue) || currentValue.length !== originalValue.length) return true;
1883
+
1884
+ // Fast array comparison for primitive types
1885
+ for (let i = 0; i < currentValue.length; i++) {
1886
+ if (currentValue[i] !== originalValue[i]) return true;
1887
+ }
1888
+ continue;
1889
+ }
1890
+
1891
+ // Handle objects (shallow comparison only) - least common
1892
+ if (currentType === 'object') {
1893
+ if (typeof originalValue !== 'object') return true;
1894
+
1895
+ // Fast object comparison using for...in
1896
+ for (const objKey in currentValue) {
1897
+ if (currentValue[objKey] !== originalValue[objKey]) return true;
1898
+ }
1899
+ // Check if original has extra keys
1900
+ for (const objKey in originalValue) {
1901
+ if (!(objKey in currentValue)) return true;
1902
+ }
1903
+ continue;
1904
+ }
1905
+
1906
+ // Fallback for other types
1907
+ if (currentValue !== originalValue) return true;
1908
+ }
1909
+
1910
+ // Check if original has extra keys (only if we haven't found differences yet)
1911
+ for (const key in original) {
1912
+ if (!(key in current)) return true;
1913
+ }
1914
+ return false;
1915
+ }
1916
+
1917
+ /**
1918
+ * Extract field names from query criteria
1919
+ * @private
1920
+ */
1921
+ _extractQueryFields(criteria) {
1922
+ const fields = new Set();
1923
+ const extractFromObject = obj => {
1924
+ for (const [key, value] of Object.entries(obj)) {
1925
+ if (key.startsWith('$')) {
1926
+ // Handle logical operators
1927
+ if (Array.isArray(value)) {
1928
+ value.forEach(item => {
1929
+ if (typeof item === 'object' && item !== null) {
1930
+ extractFromObject(item);
1931
+ }
1932
+ });
1933
+ } else if (typeof value === 'object' && value !== null) {
1934
+ extractFromObject(value);
1935
+ }
1936
+ } else {
1937
+ // Regular field
1938
+ fields.add(key);
1939
+ }
1940
+ }
1941
+ };
1942
+ extractFromObject(criteria);
1943
+ return Array.from(fields);
1944
+ }
1945
+
1946
+ /**
1947
+ * Update records matching criteria
1948
+ */
1949
+ async update(criteria, updateData) {
1950
+ this._validateInitialization('update');
1951
+ return this.operationQueue.enqueue(async () => {
1952
+ this.isInsideOperationQueue = true;
1953
+ try {
1954
+ const startTime = Date.now();
1955
+ if (this.opts.debugMode) {
1956
+ console.log(`🔄 UPDATE START: criteria=${JSON.stringify(criteria)}, updateData=${JSON.stringify(updateData)}`);
1957
+ }
1958
+
1959
+ // CRITICAL FIX: Validate state before update operation
1960
+ this.validateState();
1961
+
1962
+ // CRITICAL FIX: If there's data to save, call save() to persist it
1963
+ // Only save if there are actual records in writeBuffer
1964
+ if (this.shouldSave && this.writeBuffer.length > 0) {
1965
+ if (this.opts.debugMode) {
1966
+ console.log(`🔄 UPDATE: Calling save() before update - writeBuffer.length=${this.writeBuffer.length}`);
1967
+ }
1968
+ const saveStart = Date.now();
1969
+ await this.save(false); // Use save(false) since we're already in queue
1970
+ if (this.opts.debugMode) {
1971
+ console.log(`🔄 UPDATE: Save completed in ${Date.now() - saveStart}ms`);
1972
+ }
1973
+ }
1974
+ if (this.opts.debugMode) {
1975
+ console.log(`🔄 UPDATE: Starting find() - writeBuffer=${this.writeBuffer.length}`);
1976
+ }
1977
+ const findStart = Date.now();
1978
+ // CRITICAL FIX: Get raw records without term restoration for update operations
1979
+ const records = await this.find(criteria, {
1980
+ restoreTerms: false
1981
+ });
1982
+ if (this.opts.debugMode) {
1983
+ console.log(`🔄 UPDATE: Find completed in ${Date.now() - findStart}ms, found ${records.length} records`);
1984
+ }
1985
+ const updatedRecords = [];
1986
+ for (const record of records) {
1987
+ const recordStart = Date.now();
1988
+ if (this.opts.debugMode) {
1989
+ console.log(`🔄 UPDATE: Processing record ${record.id}`);
1990
+ }
1991
+ const updated = {
1992
+ ...record,
1993
+ ...updateData
1994
+ };
1995
+
1996
+ // Process term mapping for update
1997
+ const termMappingStart = Date.now();
1998
+ this.processTermMapping(updated, true, record);
1999
+ if (this.opts.debugMode) {
2000
+ console.log(`🔄 UPDATE: Term mapping completed in ${Date.now() - termMappingStart}ms`);
2001
+ }
2002
+
2003
+ // CRITICAL FIX: Remove old terms from index before adding new ones
2004
+ if (this.indexManager) {
2005
+ await this.indexManager.remove(record);
2006
+ if (this.opts.debugMode) {
2007
+ console.log(`🔄 UPDATE: Removed old terms from index for record ${record.id}`);
2008
+ }
2009
+ }
2010
+
2011
+ // Update record in writeBuffer or add to writeBuffer if not present
2012
+ const index = this.writeBuffer.findIndex(r => r.id === record.id);
2013
+ let lineNumber = null;
2014
+ if (index !== -1) {
2015
+ // Record is already in writeBuffer, update it
2016
+ this.writeBuffer[index] = updated;
2017
+ lineNumber = index;
2018
+ if (this.opts.debugMode) {
2019
+ console.log(`🔄 UPDATE: Updated existing writeBuffer record at index ${index}`);
2020
+ }
2021
+ } else {
2022
+ // Record is in file, add updated version to writeBuffer
2023
+ // This will ensure the updated record is saved and replaces the file version
2024
+ this.writeBuffer.push(updated);
2025
+ lineNumber = this.writeBuffer.length - 1;
2026
+ if (this.opts.debugMode) {
2027
+ console.log(`🔄 UPDATE: Added new record to writeBuffer at index ${lineNumber}`);
2028
+ }
2029
+ }
2030
+ const indexUpdateStart = Date.now();
2031
+ await this.indexManager.update(record, updated, lineNumber);
2032
+ if (this.opts.debugMode) {
2033
+ console.log(`🔄 UPDATE: Index update completed in ${Date.now() - indexUpdateStart}ms`);
2034
+ }
2035
+ updatedRecords.push(updated);
2036
+ if (this.opts.debugMode) {
2037
+ console.log(`🔄 UPDATE: Record ${record.id} completed in ${Date.now() - recordStart}ms`);
2038
+ }
2039
+ }
2040
+ this.shouldSave = true;
2041
+ this.performanceStats.operations++;
2042
+ if (this.opts.debugMode) {
2043
+ console.log(`🔄 UPDATE COMPLETED: ${updatedRecords.length} records updated in ${Date.now() - startTime}ms`);
2044
+ }
2045
+ this.emit('updated', updatedRecords);
2046
+ return updatedRecords;
2047
+ } finally {
2048
+ this.isInsideOperationQueue = false;
2049
+ }
2050
+ });
2051
+ }
2052
+
2053
+ /**
2054
+ * Delete records matching criteria
2055
+ */
2056
+ async delete(criteria) {
2057
+ this._validateInitialization('delete');
2058
+ return this.operationQueue.enqueue(async () => {
2059
+ this.isInsideOperationQueue = true;
2060
+ try {
2061
+ // CRITICAL FIX: Validate state before delete operation
2062
+ this.validateState();
2063
+ const records = await this.find(criteria);
2064
+ const deletedIds = [];
2065
+ if (this.opts.debugMode) {
2066
+ console.log(`🗑️ Delete operation: found ${records.length} records to delete`);
2067
+ console.log(`🗑️ Records to delete:`, records.map(r => ({
2068
+ id: r.id,
2069
+ name: r.name
2070
+ })));
2071
+ console.log(`🗑️ Current writeBuffer length: ${this.writeBuffer.length}`);
2072
+ console.log(`🗑️ Current deletedIds:`, Array.from(this.deletedIds));
2073
+ }
2074
+ for (const record of records) {
2075
+ // Remove term mapping
2076
+ this.removeTermMapping(record);
2077
+ await this.indexManager.remove(record);
2078
+
2079
+ // Remove record from writeBuffer or mark as deleted
2080
+ const index = this.writeBuffer.findIndex(r => r.id === record.id);
2081
+ if (index !== -1) {
2082
+ this.writeBuffer.splice(index, 1);
2083
+ if (this.opts.debugMode) {
2084
+ console.log(`🗑️ Removed record ${record.id} from writeBuffer`);
2085
+ }
2086
+ } else {
2087
+ // If record is not in writeBuffer (was saved), mark it as deleted
2088
+ this.deletedIds.add(record.id);
2089
+ if (this.opts.debugMode) {
2090
+ console.log(`🗑️ Marked record ${record.id} as deleted (not in writeBuffer)`);
2091
+ }
2092
+ }
2093
+ deletedIds.push(record.id);
2094
+ }
2095
+ if (this.opts.debugMode) {
2096
+ console.log(`🗑️ After delete: writeBuffer length: ${this.writeBuffer.length}, deletedIds:`, Array.from(this.deletedIds));
2097
+ }
2098
+ this.shouldSave = true;
2099
+ this.performanceStats.operations++;
2100
+ this.emit('deleted', deletedIds);
2101
+ return deletedIds;
2102
+ } finally {
2103
+ this.isInsideOperationQueue = false;
2104
+ }
2105
+ });
2106
+ }
2107
+
2108
+ /**
2109
+ * Generate a unique ID
2110
+ */
2111
+ generateId() {
2112
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
2113
+ }
2114
+
2115
+ /**
2116
+ * Apply schema enforcement to a record
2117
+ * Converts object to array and back to enforce schema (remove extra fields, add undefined for missing fields)
2118
+ */
2119
+ applySchemaEnforcement(record) {
2120
+ // Only apply schema enforcement if fields configuration is explicitly provided
2121
+ if (!this.opts.fields) {
2122
+ return record; // No schema enforcement without explicit fields configuration
2123
+ }
2124
+ if (!this.serializer || !this.serializer.schemaManager || !this.serializer.schemaManager.isInitialized) {
2125
+ return record; // No schema enforcement if schema not initialized
2126
+ }
2127
+
2128
+ // Convert to array format (enforces schema)
2129
+ const arrayFormat = this.serializer.convertToArrayFormat(record);
2130
+
2131
+ // Convert back to object (only schema fields will be present)
2132
+ const enforcedRecord = this.serializer.convertFromArrayFormat(arrayFormat);
2133
+
2134
+ // Preserve the ID if it exists
2135
+ if (record.id) {
2136
+ enforcedRecord.id = record.id;
2137
+ }
2138
+ return enforcedRecord;
2139
+ }
2140
+
2141
+ /**
2142
+ * Initialize schema for array-based serialization
2143
+ */
2144
+ initializeSchema() {
2145
+ if (!this.serializer || !this.serializer.schemaManager) {
2146
+ return;
2147
+ }
2148
+
2149
+ // Try to get schema from options first
2150
+ if (this.opts.schema && Array.isArray(this.opts.schema)) {
2151
+ this.serializer.initializeSchema(this.opts.schema);
2152
+ if (this.opts.debugMode) {
2153
+ console.log(`🔍 Schema initialized from options: ${this.opts.schema.join(', ')} [${this.instanceId}]`);
2154
+ }
2155
+ return;
2156
+ }
2157
+
2158
+ // Try to initialize from fields configuration (new format)
2159
+ if (this.opts.fields && typeof this.opts.fields === 'object') {
2160
+ const fieldNames = Object.keys(this.opts.fields);
2161
+ if (fieldNames.length > 0) {
2162
+ this.serializer.initializeSchema(fieldNames);
2163
+ if (this.opts.debugMode) {
2164
+ console.log(`🔍 Schema initialized from fields: ${fieldNames.join(', ')} [${this.instanceId}]`);
2165
+ }
2166
+ return;
2167
+ }
2168
+ }
2169
+
2170
+ // Try to auto-detect schema from existing data
2171
+ if (this.data && this.data.length > 0) {
2172
+ this.serializer.initializeSchema(this.data, true); // autoDetect = true
2173
+ if (this.opts.debugMode) {
2174
+ console.log(`🔍 Schema auto-detected from data: ${this.serializer.getSchema().join(', ')} [${this.instanceId}]`);
2175
+ }
2176
+ return;
2177
+ }
2178
+
2179
+ // CRITICAL FIX: Don't initialize schema from indexes
2180
+ // This was causing data loss because only indexed fields were preserved
2181
+ // Let schema be auto-detected from actual data instead
2182
+
2183
+ if (this.opts.debugMode) {
2184
+ console.log(`🔍 No schema initialization possible - will auto-detect on first insert [${this.instanceId}]`);
2185
+ }
2186
+ }
2187
+
2188
+ /**
2189
+ * Get database length (number of records)
2190
+ */
2191
+ get length() {
2192
+ // Return total records: writeBuffer + saved records
2193
+ // writeBuffer contains unsaved records
2194
+ // For saved records, use the length of offsets array (number of saved records)
2195
+ const savedRecords = this.offsets.length;
2196
+ const writeBufferRecords = this.writeBuffer.length;
2197
+
2198
+ // CRITICAL FIX: Validate that offsets array is consistent with actual data
2199
+ // This prevents the bug where database reassignment causes desynchronization
2200
+ if (this.initialized && savedRecords > 0) {
2201
+ try {
2202
+ // Check if the offsets array is consistent with the actual file
2203
+ // If offsets exist but file is empty or corrupted, reset offsets
2204
+ if (this.fileHandler && this.fileHandler.file) {
2205
+ try {
2206
+ // Use synchronous file stats to check if file is empty
2207
+ const stats = _fs.default.statSync(this.fileHandler.file);
2208
+ if (stats && stats.size === 0 && savedRecords > 0) {
2209
+ // File is empty but offsets array has records - this is the bug condition
2210
+ if (this.opts.debugMode) {
2211
+ console.log(`🔧 LENGTH FIX: Detected desynchronized offsets (${savedRecords} records) with empty file, resetting offsets`);
2212
+ }
2213
+ this.offsets = [];
2214
+ return writeBufferRecords; // Return only writeBuffer records
2215
+ }
2216
+ } catch (fileError) {
2217
+ // File doesn't exist or can't be read - reset offsets
2218
+ if (savedRecords > 0) {
2219
+ if (this.opts.debugMode) {
2220
+ console.log(`🔧 LENGTH FIX: File doesn't exist but offsets array has ${savedRecords} records, resetting offsets`);
2221
+ }
2222
+ this.offsets = [];
2223
+ return writeBufferRecords;
2224
+ }
2225
+ }
2226
+ }
2227
+ } catch (error) {
2228
+ // If we can't validate, fall back to the original behavior
2229
+ if (this.opts.debugMode) {
2230
+ console.log(`🔧 LENGTH FIX: Could not validate offsets, using original calculation: ${error.message}`);
2231
+ }
2232
+ }
2233
+ }
2234
+ return writeBufferRecords + savedRecords;
2235
+ }
2236
+
2237
+ /**
2238
+ * Calculate current writeBuffer size in bytes (similar to published v1.1.0)
2239
+ */
2240
+ currentWriteBufferSize() {
2241
+ if (!this.writeBuffer || this.writeBuffer.length === 0) {
2242
+ return 0;
2243
+ }
2244
+
2245
+ // Calculate total size of all records in writeBuffer
2246
+ let totalSize = 0;
2247
+ for (const record of this.writeBuffer) {
2248
+ if (record) {
2249
+ // SPACE OPTIMIZATION: Remove term IDs before size calculation
2250
+ const cleanRecord = this.removeTermIdsForSerialization(record);
2251
+ const recordJson = JSON.stringify(cleanRecord) + '\n';
2252
+ totalSize += Buffer.byteLength(recordJson, 'utf8');
2253
+ }
2254
+ }
2255
+ return totalSize;
2256
+ }
2257
+
2258
+ /**
2259
+ * Get database statistics
2260
+ */
2261
+ getStats() {
2262
+ const stats = {
2263
+ records: this.writeBuffer.length,
2264
+ writeBufferSize: this.currentWriteBufferSize(),
2265
+ maxMemoryUsage: this.opts.maxMemoryUsage,
2266
+ performance: this.performanceStats,
2267
+ lastSave: this.lastSaveTime,
2268
+ shouldSave: this.shouldSave,
2269
+ initialized: this.initialized
2270
+ };
2271
+
2272
+ // Add term mapping stats if enabled
2273
+ if (this.opts.termMapping && this.termManager) {
2274
+ stats.termMapping = this.termManager.getStats();
2275
+ }
2276
+ return stats;
2277
+ }
2278
+
2279
+ /**
2280
+ * Initialize database (alias for initialize for backward compatibility)
2281
+ */
2282
+ async init() {
2283
+ return this.initialize();
2284
+ }
2285
+
2286
+ /**
2287
+ * Destroy database - DESTRUCTIVE MODE
2288
+ * Assumes save() has already been called by user
2289
+ * If anything is still active, it indicates a bug - log error and force cleanup
2290
+ */
2291
+ async destroy() {
2292
+ if (this.destroyed) return;
2293
+
2294
+ // Mark as destroying immediately to prevent new operations
2295
+ this.destroying = true;
2296
+
2297
+ // Wait for all active insert sessions to complete before destroying
2298
+ if (this.activeInsertSessions.size > 0) {
2299
+ if (this.opts.debugMode) {
2300
+ console.log(`⏳ destroy: Waiting for ${this.activeInsertSessions.size} active insert sessions`);
2301
+ }
2302
+ const sessionPromises = Array.from(this.activeInsertSessions).map(session => session.waitForOperations(null) // Wait indefinitely for sessions to complete
2303
+ );
2304
+ try {
2305
+ await Promise.all(sessionPromises);
2306
+ } catch (error) {
2307
+ if (this.opts.debugMode) {
2308
+ console.log(`⚠️ destroy: Error waiting for sessions: ${error.message}`);
2309
+ }
2310
+ // Continue with destruction even if sessions have issues
2311
+ }
2312
+
2313
+ // Destroy all active sessions
2314
+ for (const session of this.activeInsertSessions) {
2315
+ session.destroy();
2316
+ }
2317
+ this.activeInsertSessions.clear();
2318
+ }
2319
+
2320
+ // CRITICAL FIX: Add timeout protection to prevent destroy() from hanging
2321
+ const destroyPromise = this._performDestroy();
2322
+ let timeoutHandle = null;
2323
+ const timeoutPromise = new Promise((_, reject) => {
2324
+ timeoutHandle = setTimeout(() => {
2325
+ reject(new Error('Destroy operation timed out after 5 seconds'));
2326
+ }, 5000);
2327
+ });
2328
+ try {
2329
+ await Promise.race([destroyPromise, timeoutPromise]);
2330
+ } catch (error) {
2331
+ if (error.message === 'Destroy operation timed out after 5 seconds') {
2332
+ console.error('🚨 DESTROY TIMEOUT: Force destroying database after timeout');
2333
+ // Force mark as destroyed even if cleanup failed
2334
+ this.destroyed = true;
2335
+ this.destroying = false;
2336
+ return;
2337
+ }
2338
+ throw error;
2339
+ } finally {
2340
+ // Clear the timeout to prevent Jest open handle warning
2341
+ if (timeoutHandle) {
2342
+ clearTimeout(timeoutHandle);
2343
+ }
2344
+ }
2345
+ }
2346
+
2347
+ /**
2348
+ * Internal destroy implementation
2349
+ */
2350
+ async _performDestroy() {
2351
+ try {
2352
+ // CRITICAL: Check for bugs - anything active indicates save() didn't work properly
2353
+ const bugs = [];
2354
+
2355
+ // Check for pending data that should have been saved
2356
+ if (this.writeBuffer.length > 0) {
2357
+ const bug = `BUG: writeBuffer has ${this.writeBuffer.length} records - save() should have cleared this`;
2358
+ bugs.push(bug);
2359
+ console.error(`🚨 ${bug}`);
2360
+ }
2361
+
2362
+ // Check for pending operations that should have completed
2363
+ if (this.pendingOperations.size > 0) {
2364
+ const bug = `BUG: ${this.pendingOperations.size} pending operations - save() should have completed these`;
2365
+ bugs.push(bug);
2366
+ console.error(`🚨 ${bug}`);
2367
+ }
2368
+
2369
+ // Auto-save manager removed - no cleanup needed
2370
+
2371
+ // Check for active save operation
2372
+ if (this.isSaving) {
2373
+ const bug = `BUG: save operation still active - previous save() should have completed`;
2374
+ bugs.push(bug);
2375
+ console.error(`🚨 ${bug}`);
2376
+ }
2377
+
2378
+ // If bugs detected, throw error with details
2379
+ if (bugs.length > 0) {
2380
+ const errorMessage = `Database destroy() found ${bugs.length} bug(s) - save() did not complete properly:\n${bugs.join('\n')}`;
2381
+ console.error(`🚨 DESTROY ERROR: ${errorMessage}`);
2382
+ throw new Error(errorMessage);
2383
+ }
2384
+
2385
+ // FORCE DESTRUCTIVE CLEANUP - no waiting, no graceful shutdown
2386
+ if (this.opts.debugMode) {
2387
+ console.log('💥 DESTRUCTIVE DESTROY: Force cleaning up all resources');
2388
+ }
2389
+
2390
+ // Cancel all operations immediately
2391
+ this.abortController.abort();
2392
+
2393
+ // Auto-save removed - no cleanup needed
2394
+
2395
+ // Clear all buffers and data structures
2396
+ this.writeBuffer = [];
2397
+ this.writeBufferOffsets = [];
2398
+ this.writeBufferSizes = [];
2399
+ this.deletedIds.clear();
2400
+ this.pendingOperations.clear();
2401
+ this.pendingIndexUpdates = [];
2402
+
2403
+ // Force close file handlers
2404
+ if (this.fileHandler) {
2405
+ try {
2406
+ // Force close any open file descriptors
2407
+ await this.fileHandler.close?.();
2408
+ } catch (error) {
2409
+ // Ignore file close errors during destructive cleanup
2410
+ }
2411
+ }
2412
+
2413
+ // Clear all managers
2414
+ if (this.indexManager) {
2415
+ this.indexManager.clear?.();
2416
+ }
2417
+ if (this.termManager) {
2418
+ this.termManager.clear?.();
2419
+ }
2420
+ if (this.queryManager) {
2421
+ this.queryManager.clear?.();
2422
+ }
2423
+
2424
+ // Clear operation queue
2425
+ if (this.operationQueue) {
2426
+ this.operationQueue.clear?.();
2427
+ this.operationQueue = null;
2428
+ }
2429
+
2430
+ // Mark as destroyed
2431
+ this.destroyed = true;
2432
+ this.destroying = false;
2433
+ if (this.opts.debugMode) {
2434
+ console.log('💥 DESTRUCTIVE DESTROY: Database completely destroyed');
2435
+ }
2436
+ } catch (error) {
2437
+ // Even if cleanup fails, mark as destroyed
2438
+ this.destroyed = true;
2439
+ this.destroying = false;
2440
+
2441
+ // Re-throw the error so user knows about the bug
2442
+ throw error;
2443
+ }
2444
+ }
2445
+
2446
+ /**
2447
+ * Find one record
2448
+ */
2449
+ async findOne(criteria, options = {}) {
2450
+ this._validateInitialization('findOne');
2451
+ const results = await this.find(criteria, {
2452
+ ...options,
2453
+ limit: 1
2454
+ });
2455
+ return results.length > 0 ? results[0] : null;
2456
+ }
2457
+
2458
+ /**
2459
+ * Count records
2460
+ */
2461
+ async count(criteria = {}, options = {}) {
2462
+ this._validateInitialization('count');
2463
+ const results = await this.find(criteria, options);
2464
+ return results.length;
2465
+ }
2466
+
2467
+ /**
2468
+ * Wait for all pending operations to complete
2469
+ */
2470
+ async _waitForPendingOperations() {
2471
+ if (this.operationQueue && this.operationQueue.getQueueLength() > 0) {
2472
+ if (this.opts.debugMode) {
2473
+ console.log('💾 Save: Waiting for pending operations to complete');
2474
+ }
2475
+ // CRITICAL FIX: Wait without timeout to ensure all operations complete
2476
+ // This prevents race conditions and data loss
2477
+ await this.operationQueue.waitForCompletion(null);
2478
+
2479
+ // Verify queue is actually empty
2480
+ if (this.operationQueue.getQueueLength() > 0) {
2481
+ throw new Error('Operation queue not empty after wait');
2482
+ }
2483
+ }
2484
+ }
2485
+
2486
+ /**
2487
+ * Flush write buffer completely with smart detection of ongoing insertions
2488
+ */
2489
+ async _flushWriteBufferCompletely() {
2490
+ // Force complete flush of write buffer with intelligent detection
2491
+ let attempts = 0;
2492
+ const maxStuckAttempts = 5; // Maximum attempts with identical data (only protection against infinite loops)
2493
+ let stuckAttempts = 0;
2494
+ let lastBufferSample = null;
2495
+
2496
+ // CRITICAL FIX: Remove maxAttempts limit - only stop when buffer is empty or truly stuck
2497
+ while (this.writeBuffer.length > 0) {
2498
+ const currentLength = this.writeBuffer.length;
2499
+ const currentSample = this._getBufferSample(); // Get lightweight sample
2500
+
2501
+ // Process write buffer items
2502
+ await this._processWriteBuffer();
2503
+
2504
+ // Check if buffer is actually stuck (same data) vs new data being added
2505
+ if (this.writeBuffer.length === currentLength) {
2506
+ // Check if the data is identical (stuck) or new data was added
2507
+ if (this._isBufferSampleIdentical(currentSample, lastBufferSample)) {
2508
+ stuckAttempts++;
2509
+ if (this.opts.debugMode) {
2510
+ console.log(`💾 Flush: Buffer appears stuck (identical data), attempt ${stuckAttempts}/${maxStuckAttempts}`);
2511
+ }
2512
+ if (stuckAttempts >= maxStuckAttempts) {
2513
+ throw new Error(`Write buffer flush stuck - identical data detected after ${maxStuckAttempts} attempts`);
2514
+ }
2515
+ } else {
2516
+ // New data was added, reset stuck counter
2517
+ stuckAttempts = 0;
2518
+ if (this.opts.debugMode) {
2519
+ console.log(`💾 Flush: New data detected, continuing flush (${this.writeBuffer.length} items remaining)`);
2520
+ }
2521
+ }
2522
+ lastBufferSample = currentSample;
2523
+ } else {
2524
+ // Progress was made, reset stuck counter
2525
+ stuckAttempts = 0;
2526
+ lastBufferSample = null;
2527
+ if (this.opts.debugMode) {
2528
+ console.log(`💾 Flush: Progress made, ${currentLength - this.writeBuffer.length} items processed, ${this.writeBuffer.length} remaining`);
2529
+ }
2530
+ }
2531
+ attempts++;
2532
+
2533
+ // Small delay to allow ongoing operations to complete
2534
+ if (this.writeBuffer.length > 0) {
2535
+ await new Promise(resolve => setTimeout(resolve, 10));
2536
+ }
2537
+ }
2538
+
2539
+ // CRITICAL FIX: Remove the artificial limit check - buffer should be empty by now
2540
+ // If we reach here, the buffer is guaranteed to be empty due to the while condition
2541
+
2542
+ if (this.opts.debugMode) {
2543
+ console.log(`💾 Flush completed successfully after ${attempts} attempts`);
2544
+ }
2545
+ }
2546
+
2547
+ /**
2548
+ * Get a lightweight sample of the write buffer for comparison
2549
+ * @returns {Object} - Sample data for comparison
2550
+ */
2551
+ _getBufferSample() {
2552
+ if (!this.writeBuffer || this.writeBuffer.length === 0) {
2553
+ return null;
2554
+ }
2555
+
2556
+ // Create a lightweight sample using first few items and their IDs
2557
+ const sampleSize = Math.min(5, this.writeBuffer.length);
2558
+ const sample = {
2559
+ length: this.writeBuffer.length,
2560
+ firstIds: [],
2561
+ lastIds: [],
2562
+ checksum: 0
2563
+ };
2564
+
2565
+ // Sample first few items
2566
+ for (let i = 0; i < sampleSize; i++) {
2567
+ const item = this.writeBuffer[i];
2568
+ if (item && item.id) {
2569
+ sample.firstIds.push(item.id);
2570
+ // Simple checksum using ID hash
2571
+ sample.checksum += item.id.toString().split('').reduce((a, b) => a + b.charCodeAt(0), 0);
2572
+ }
2573
+ }
2574
+
2575
+ // Sample last few items if buffer is large
2576
+ if (this.writeBuffer.length > sampleSize) {
2577
+ for (let i = Math.max(0, this.writeBuffer.length - sampleSize); i < this.writeBuffer.length; i++) {
2578
+ const item = this.writeBuffer[i];
2579
+ if (item && item.id) {
2580
+ sample.lastIds.push(item.id);
2581
+ sample.checksum += item.id.toString().split('').reduce((a, b) => a + b.charCodeAt(0), 0);
2582
+ }
2583
+ }
2584
+ }
2585
+ return sample;
2586
+ }
2587
+
2588
+ /**
2589
+ * Check if two buffer samples are identical (indicating stuck flush)
2590
+ * @param {Object} sample1 - First sample
2591
+ * @param {Object} sample2 - Second sample
2592
+ * @returns {boolean} - True if samples are identical
2593
+ */
2594
+ _isBufferSampleIdentical(sample1, sample2) {
2595
+ if (!sample1 || !sample2) {
2596
+ return false;
2597
+ }
2598
+
2599
+ // Quick checks: different lengths or checksums mean different data
2600
+ if (sample1.length !== sample2.length || sample1.checksum !== sample2.checksum) {
2601
+ return false;
2602
+ }
2603
+
2604
+ // Compare first IDs
2605
+ if (sample1.firstIds.length !== sample2.firstIds.length) {
2606
+ return false;
2607
+ }
2608
+ for (let i = 0; i < sample1.firstIds.length; i++) {
2609
+ if (sample1.firstIds[i] !== sample2.firstIds[i]) {
2610
+ return false;
2611
+ }
2612
+ }
2613
+
2614
+ // Compare last IDs
2615
+ if (sample1.lastIds.length !== sample2.lastIds.length) {
2616
+ return false;
2617
+ }
2618
+ for (let i = 0; i < sample1.lastIds.length; i++) {
2619
+ if (sample1.lastIds[i] !== sample2.lastIds[i]) {
2620
+ return false;
2621
+ }
2622
+ }
2623
+ return true;
2624
+ }
2625
+
2626
+ /**
2627
+ * Process write buffer items
2628
+ */
2629
+ async _processWriteBuffer() {
2630
+ // Process write buffer items without loading entire file
2631
+ // OPTIMIZATION: Use Set directly for both processing and lookup - single variable, better performance
2632
+ const itemsToProcess = new Set(this.writeBuffer);
2633
+
2634
+ // CRITICAL FIX: Don't clear writeBuffer immediately - wait for processing to complete
2635
+ // This prevents race conditions where new operations arrive while old ones are still processing
2636
+
2637
+ // OPTIMIZATION: Separate buffer items from object items for batch processing
2638
+ const bufferItems = [];
2639
+ const objectItems = [];
2640
+ for (const item of itemsToProcess) {
2641
+ if (Buffer.isBuffer(item)) {
2642
+ bufferItems.push(item);
2643
+ } else if (typeof item === 'object' && item !== null) {
2644
+ objectItems.push(item);
2645
+ }
2646
+ }
2647
+
2648
+ // Process buffer items individually (they're already optimized)
2649
+ for (const buffer of bufferItems) {
2650
+ await this._processBufferItem(buffer);
2651
+ }
2652
+
2653
+ // OPTIMIZATION: Process all object items in a single write operation
2654
+ if (objectItems.length > 0) {
2655
+ await this._processObjectItemsBatch(objectItems);
2656
+ }
2657
+
2658
+ // CRITICAL FIX: Only remove processed items from writeBuffer after all async operations complete
2659
+ // OPTIMIZATION: Use Set.has() for O(1) lookup - same Set used for processing
2660
+ const beforeLength = this.writeBuffer.length;
2661
+ this.writeBuffer = this.writeBuffer.filter(item => !itemsToProcess.has(item));
2662
+ const afterLength = this.writeBuffer.length;
2663
+ if (this.opts.debugMode && beforeLength !== afterLength) {
2664
+ console.log(`💾 _processWriteBuffer: Removed ${beforeLength - afterLength} items from writeBuffer (${beforeLength} -> ${afterLength})`);
2665
+ }
2666
+ }
2667
+
2668
+ /**
2669
+ * Process individual buffer item
2670
+ */
2671
+ async _processBufferItem(buffer) {
2672
+ // Process buffer item without loading entire file
2673
+ // This ensures we don't load the entire data file into memory
2674
+ if (this.fileHandler) {
2675
+ // Use writeDataAsync for non-blocking I/O
2676
+ await this.fileHandler.writeDataAsync(buffer);
2677
+ }
2678
+ }
2679
+
2680
+ /**
2681
+ * Process individual object item
2682
+ */
2683
+ async _processObjectItem(obj) {
2684
+ // Process object item without loading entire file
2685
+ if (this.fileHandler) {
2686
+ // SPACE OPTIMIZATION: Remove term IDs before serialization
2687
+ const cleanRecord = this.removeTermIdsForSerialization(obj);
2688
+ const jsonString = this.serializer.serialize(cleanRecord).toString('utf8');
2689
+ // Use writeDataAsync for non-blocking I/O
2690
+ await this.fileHandler.writeDataAsync(Buffer.from(jsonString, 'utf8'));
2691
+ }
2692
+ }
2693
+
2694
+ /**
2695
+ * Process multiple object items in a single batch write operation
2696
+ */
2697
+ async _processObjectItemsBatch(objects) {
2698
+ if (!this.fileHandler || objects.length === 0) return;
2699
+
2700
+ // OPTIMIZATION: Combine all objects into a single buffer for one write operation
2701
+ // SPACE OPTIMIZATION: Remove term IDs before serialization
2702
+ const jsonStrings = objects.map(obj => this.serializer.serialize(this.removeTermIdsForSerialization(obj)).toString('utf8'));
2703
+ const combinedString = jsonStrings.join('');
2704
+
2705
+ // CRITICAL FIX: Validate that the combined string ends with newline
2706
+ const validatedString = combinedString.endsWith('\n') ? combinedString : combinedString + '\n';
2707
+ const buffer = Buffer.from(validatedString, 'utf8');
2708
+
2709
+ // Single write operation for all objects
2710
+ await this.fileHandler.writeDataAsync(buffer);
2711
+ }
2712
+
2713
+ /**
2714
+ * Wait for all I/O operations to complete
2715
+ */
2716
+ async _waitForIOCompletion() {
2717
+ // Wait for all file operations to complete
2718
+ if (this.fileHandler && this.fileHandler.fileMutex) {
2719
+ await this.fileHandler.fileMutex.runExclusive(async () => {
2720
+ // Ensure all pending file operations complete
2721
+ await new Promise(resolve => setTimeout(resolve, 50));
2722
+ });
2723
+ }
2724
+ }
2725
+
2726
+ /**
2727
+ * CRITICAL FIX: Safe fallback method to load existing records when _streamExistingRecords fails
2728
+ * This prevents data loss by attempting alternative methods to preserve existing data
2729
+ */
2730
+ async _loadExistingRecordsFallback(deletedIdsSnapshot, writeBufferSnapshot) {
2731
+ const existingRecords = [];
2732
+ try {
2733
+ if (this.opts.debugMode) {
2734
+ console.log(`💾 Save: Attempting fallback method to load existing records`);
2735
+ }
2736
+
2737
+ // Method 1: Try to read the entire file and filter
2738
+ if (this.fileHandler.exists()) {
2739
+ const fs = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
2740
+ const fileContent = await fs.promises.readFile(this.normalizedFile, 'utf8');
2741
+ const lines = fileContent.split('\n').filter(line => line.trim());
2742
+ for (let i = 0; i < lines.length && i < this.offsets.length; i++) {
2743
+ try {
2744
+ const record = this.serializer.deserialize(lines[i]);
2745
+ if (record && !deletedIdsSnapshot.has(record.id)) {
2746
+ // Check if this record is not being updated in writeBuffer
2747
+ const updatedRecord = writeBufferSnapshot.find(r => r.id === record.id);
2748
+ if (!updatedRecord) {
2749
+ existingRecords.push(record);
2750
+ }
2751
+ }
2752
+ } catch (error) {
2753
+ // Skip invalid lines
2754
+ if (this.opts.debugMode) {
2755
+ console.log(`💾 Save: Skipping invalid line ${i} in fallback:`, error.message);
2756
+ }
2757
+ }
2758
+ }
2759
+ }
2760
+ if (this.opts.debugMode) {
2761
+ console.log(`💾 Save: Fallback method loaded ${existingRecords.length} existing records`);
2762
+ }
2763
+ return existingRecords;
2764
+ } catch (error) {
2765
+ if (this.opts.debugMode) {
2766
+ console.log(`💾 Save: Fallback method failed:`, error.message);
2767
+ }
2768
+ // Return empty array as last resort - better than losing all data
2769
+ return [];
2770
+ }
2771
+ }
2772
+
2773
+ /**
2774
+ * Stream existing records without loading entire file into memory
2775
+ * Optimized with group ranging and reduced JSON parsing
2776
+ */
2777
+ async _streamExistingRecords(deletedIdsSnapshot, writeBufferSnapshot) {
2778
+ const existingRecords = [];
2779
+ if (this.offsets.length === 0) {
2780
+ return existingRecords;
2781
+ }
2782
+
2783
+ // OPTIMIZATION: Pre-allocate array with known size (but don't set length to avoid undefined slots)
2784
+ // existingRecords.length = this.offsets.length
2785
+
2786
+ // Create a map of updated records for quick lookup
2787
+ const updatedRecordsMap = new Map();
2788
+ writeBufferSnapshot.forEach(record => {
2789
+ updatedRecordsMap.set(record.id, record);
2790
+ });
2791
+
2792
+ // OPTIMIZATION: Cache file stats to avoid repeated stat() calls
2793
+ let fileSize = 0;
2794
+ if (this._cachedFileStats && this._cachedFileStats.timestamp > Date.now() - 1000) {
2795
+ // Use cached stats if less than 1 second old
2796
+ fileSize = this._cachedFileStats.size;
2797
+ } else {
2798
+ // Get fresh stats and cache them
2799
+ const fileStats = (await this.fileHandler.exists()) ? await _fs.default.promises.stat(this.normalizedFile) : null;
2800
+ fileSize = fileStats ? fileStats.size : 0;
2801
+ this._cachedFileStats = {
2802
+ size: fileSize,
2803
+ timestamp: Date.now()
2804
+ };
2805
+ }
2806
+
2807
+ // CRITICAL FIX: Ensure indexOffset is consistent with actual file size
2808
+ if (this.indexOffset > fileSize) {
2809
+ if (this.opts.debugMode) {
2810
+ console.log(`💾 Save: Correcting indexOffset from ${this.indexOffset} to ${fileSize} (file size)`);
2811
+ }
2812
+ this.indexOffset = fileSize;
2813
+ }
2814
+
2815
+ // Build ranges array for group reading
2816
+ const ranges = [];
2817
+ for (let i = 0; i < this.offsets.length; i++) {
2818
+ const offset = this.offsets[i];
2819
+ let nextOffset = i + 1 < this.offsets.length ? this.offsets[i + 1] : this.indexOffset;
2820
+ if (this.opts.debugMode) {
2821
+ console.log(`💾 Save: Building range for record ${i}: offset=${offset}, nextOffset=${nextOffset}`);
2822
+ }
2823
+
2824
+ // CRITICAL FIX: Handle case where indexOffset is 0 (new database without index)
2825
+ if (nextOffset === 0 && i + 1 >= this.offsets.length) {
2826
+ // For the last record when there's no index yet, we need to find the actual end
2827
+ // Read a bit more data to find the newline character that ends the record
2828
+ const searchEnd = Math.min(offset + 1000, fileSize); // Search up to 1000 bytes ahead
2829
+ if (searchEnd > offset) {
2830
+ try {
2831
+ const searchBuffer = await this.fileHandler.readRange(offset, searchEnd);
2832
+ const searchText = searchBuffer.toString('utf8');
2833
+
2834
+ // Look for the end of the JSON record (closing brace followed by newline or end of data)
2835
+ let recordEnd = -1;
2836
+ let braceCount = 0;
2837
+ let inString = false;
2838
+ let escapeNext = false;
2839
+ for (let j = 0; j < searchText.length; j++) {
2840
+ const char = searchText[j];
2841
+ if (escapeNext) {
2842
+ escapeNext = false;
2843
+ continue;
2844
+ }
2845
+ if (char === '\\') {
2846
+ escapeNext = true;
2847
+ continue;
2848
+ }
2849
+ if (char === '"' && !escapeNext) {
2850
+ inString = !inString;
2851
+ continue;
2852
+ }
2853
+ if (!inString) {
2854
+ if (char === '{') {
2855
+ braceCount++;
2856
+ } else if (char === '}') {
2857
+ braceCount--;
2858
+ if (braceCount === 0) {
2859
+ // Found the end of the JSON object
2860
+ recordEnd = j + 1;
2861
+ break;
2862
+ }
2863
+ }
2864
+ }
2865
+ }
2866
+ if (recordEnd !== -1) {
2867
+ nextOffset = offset + recordEnd;
2868
+ } else {
2869
+ // If we can't find the end, read to end of file
2870
+ nextOffset = fileSize;
2871
+ }
2872
+ } catch (error) {
2873
+ // Fallback to end of file if search fails
2874
+ nextOffset = fileSize;
2875
+ }
2876
+ } else {
2877
+ nextOffset = fileSize;
2878
+ }
2879
+ }
2880
+
2881
+ // Validate offset ranges
2882
+ if (offset < 0) {
2883
+ if (this.opts.debugMode) {
2884
+ console.log(`💾 Save: Skipped negative offset ${offset}`);
2885
+ }
2886
+ continue;
2887
+ }
2888
+
2889
+ // CRITICAL FIX: Allow offsets that are at or beyond file size (for new records)
2890
+ if (fileSize > 0 && offset > fileSize) {
2891
+ if (this.opts.debugMode) {
2892
+ console.log(`💾 Save: Skipped offset ${offset} beyond file size ${fileSize}`);
2893
+ }
2894
+ continue;
2895
+ }
2896
+ if (nextOffset <= offset) {
2897
+ if (this.opts.debugMode) {
2898
+ console.log(`💾 Save: Skipped invalid range [${offset}, ${nextOffset}]`);
2899
+ }
2900
+ continue;
2901
+ }
2902
+ ranges.push({
2903
+ start: offset,
2904
+ end: nextOffset,
2905
+ index: i
2906
+ });
2907
+ }
2908
+ if (ranges.length === 0) {
2909
+ return existingRecords;
2910
+ }
2911
+
2912
+ // Use group ranging for efficient reading
2913
+ const recordLines = await this.fileHandler.readRanges(ranges, async (lineString, range) => {
2914
+ if (!lineString || !lineString.trim()) {
2915
+ return null;
2916
+ }
2917
+ const trimmedLine = lineString.trim();
2918
+
2919
+ // DEBUG: Log what we're reading (temporarily enabled for debugging)
2920
+ if (this.opts.debugMode) {
2921
+ console.log(`💾 Save: Reading range ${range.start}-${range.end}, length: ${trimmedLine.length}`);
2922
+ console.log(`💾 Save: First 100 chars: ${trimmedLine.substring(0, 100)}`);
2923
+ if (trimmedLine.length > 100) {
2924
+ console.log(`💾 Save: Last 100 chars: ${trimmedLine.substring(trimmedLine.length - 100)}`);
2925
+ }
2926
+ }
2927
+
2928
+ // OPTIMIZATION: Try to extract ID without full JSON parsing
2929
+ let recordId = null;
2930
+ let needsFullParse = false;
2931
+
2932
+ // For array format, try to extract ID from array position
2933
+ if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) {
2934
+ // Array format: try to extract ID from the array
2935
+ try {
2936
+ const arrayData = JSON.parse(trimmedLine);
2937
+ if (Array.isArray(arrayData) && arrayData.length > 0) {
2938
+ // For arrays without explicit ID, use the first element as a fallback
2939
+ // or try to find the ID field if it exists
2940
+ if (arrayData.length > 2) {
2941
+ // ID is typically at position 2 in array format [age, city, id, name]
2942
+ recordId = arrayData[2];
2943
+ } else {
2944
+ // For arrays without ID field, use first element as fallback
2945
+ recordId = arrayData[0];
2946
+ }
2947
+ if (recordId !== undefined && recordId !== null) {
2948
+ recordId = String(recordId);
2949
+ // Check if this record needs full parsing (updated or deleted)
2950
+ needsFullParse = updatedRecordsMap.has(recordId) || deletedIdsSnapshot.has(recordId);
2951
+ } else {
2952
+ needsFullParse = true;
2953
+ }
2954
+ } else {
2955
+ needsFullParse = true;
2956
+ }
2957
+ } catch (e) {
2958
+ needsFullParse = true;
2959
+ }
2960
+ } else {
2961
+ // Object format: use regex for backward compatibility
2962
+ const idMatch = trimmedLine.match(/"id"\s*:\s*"([^"]+)"|"id"\s*:\s*(\d+)/);
2963
+ if (idMatch) {
2964
+ recordId = idMatch[1] || idMatch[2];
2965
+ needsFullParse = updatedRecordsMap.has(recordId) || deletedIdsSnapshot.has(recordId);
2966
+ } else {
2967
+ needsFullParse = true;
2968
+ }
2969
+ }
2970
+ if (!needsFullParse) {
2971
+ // Record is unchanged - we can avoid parsing entirely
2972
+ // Store the raw line and parse only when needed for the final result
2973
+ return {
2974
+ type: 'unchanged',
2975
+ line: trimmedLine,
2976
+ id: recordId,
2977
+ needsParse: false
2978
+ };
2979
+ }
2980
+
2981
+ // Full parsing needed for updated/deleted records
2982
+ try {
2983
+ // Use serializer to properly deserialize array format
2984
+ const record = this.serializer ? this.serializer.deserialize(trimmedLine) : JSON.parse(trimmedLine);
2985
+
2986
+ // Use record directly (no need to restore term IDs)
2987
+ const recordWithIds = record;
2988
+ if (updatedRecordsMap.has(recordWithIds.id)) {
2989
+ // Replace with updated version
2990
+ const updatedRecord = updatedRecordsMap.get(recordWithIds.id);
2991
+ if (this.opts.debugMode) {
2992
+ console.log(`💾 Save: Updated record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'})`);
2993
+ }
2994
+ return {
2995
+ type: 'updated',
2996
+ record: updatedRecord,
2997
+ id: recordWithIds.id,
2998
+ needsParse: false
2999
+ };
3000
+ } else if (!deletedIdsSnapshot.has(recordWithIds.id)) {
3001
+ // Keep existing record if not deleted
3002
+ if (this.opts.debugMode) {
3003
+ console.log(`💾 Save: Kept record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'})`);
3004
+ }
3005
+ return {
3006
+ type: 'kept',
3007
+ record: recordWithIds,
3008
+ id: recordWithIds.id,
3009
+ needsParse: false
3010
+ };
3011
+ } else {
3012
+ // Skip deleted record
3013
+ if (this.opts.debugMode) {
3014
+ console.log(`💾 Save: Skipped record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'}) - deleted`);
3015
+ }
3016
+ return {
3017
+ type: 'deleted',
3018
+ id: recordWithIds.id,
3019
+ needsParse: false
3020
+ };
3021
+ }
3022
+ } catch (jsonError) {
3023
+ // RACE CONDITION FIX: Skip records that can't be parsed due to incomplete writes
3024
+ if (this.opts.debugMode) {
3025
+ console.log(`💾 Save: Skipped corrupted record at range ${range.start}-${range.end} - ${jsonError.message}`);
3026
+ // console.log(`💾 Save: Problematic line: ${trimmedLine}`)
3027
+ }
3028
+ return null;
3029
+ }
3030
+ });
3031
+
3032
+ // Process results and build final records array
3033
+ // OPTIMIZATION: Pre-allocate arrays with known size
3034
+ const unchangedLines = [];
3035
+ const parsedRecords = [];
3036
+
3037
+ // OPTIMIZATION: Use for loop instead of Object.entries().sort() for better performance
3038
+ const sortedEntries = [];
3039
+ for (const key in recordLines) {
3040
+ if (recordLines.hasOwnProperty(key)) {
3041
+ sortedEntries.push([key, recordLines[key]]);
3042
+ }
3043
+ }
3044
+
3045
+ // OPTIMIZATION: Sort by offset position using numeric comparison
3046
+ sortedEntries.sort(([keyA], [keyB]) => parseInt(keyA) - parseInt(keyB));
3047
+
3048
+ // CRITICAL FIX: Maintain record order by processing in original offset order
3049
+ // and tracking which records are being kept vs deleted
3050
+ const keptRecords = [];
3051
+ const deletedOffsets = new Set();
3052
+ for (const [rangeKey, result] of sortedEntries) {
3053
+ if (!result) continue;
3054
+ const offset = parseInt(rangeKey);
3055
+ switch (result.type) {
3056
+ case 'unchanged':
3057
+ // Collect unchanged lines for batch processing
3058
+ unchangedLines.push(result.line);
3059
+ keptRecords.push({
3060
+ offset,
3061
+ type: 'unchanged',
3062
+ line: result.line
3063
+ });
3064
+ break;
3065
+ case 'updated':
3066
+ case 'kept':
3067
+ parsedRecords.push(result.record);
3068
+ keptRecords.push({
3069
+ offset,
3070
+ type: 'parsed',
3071
+ record: result.record
3072
+ });
3073
+ break;
3074
+ case 'deleted':
3075
+ // Track deleted records by their offset
3076
+ deletedOffsets.add(offset);
3077
+ break;
3078
+ }
3079
+ }
3080
+
3081
+ // CRITICAL FIX: Build final records array in the correct order
3082
+ // and update offsets array to match the new record order
3083
+ const newOffsets = [];
3084
+ let currentOffset = 0;
3085
+
3086
+ // OPTIMIZATION: Batch parse unchanged records for better performance
3087
+ if (unchangedLines.length > 0) {
3088
+ const batchParsedRecords = [];
3089
+ for (let i = 0; i < unchangedLines.length; i++) {
3090
+ try {
3091
+ // Use serializer to properly deserialize array format
3092
+ const record = this.serializer ? this.serializer.deserialize(unchangedLines[i]) : JSON.parse(unchangedLines[i]);
3093
+ batchParsedRecords.push(record);
3094
+ } catch (jsonError) {
3095
+ if (this.opts.debugMode) {
3096
+ console.log(`💾 Save: Failed to parse unchanged record: ${jsonError.message}`);
3097
+ }
3098
+ batchParsedRecords.push(null); // Mark as failed
3099
+ }
3100
+ }
3101
+
3102
+ // Process kept records in their original offset order
3103
+ let batchIndex = 0;
3104
+ for (const keptRecord of keptRecords) {
3105
+ let record = null;
3106
+ if (keptRecord.type === 'unchanged') {
3107
+ record = batchParsedRecords[batchIndex++];
3108
+ if (!record) continue; // Skip failed parses
3109
+ } else if (keptRecord.type === 'parsed') {
3110
+ record = keptRecord.record;
3111
+ }
3112
+ if (record && typeof record === 'object') {
3113
+ existingRecords.push(record);
3114
+ newOffsets.push(currentOffset);
3115
+ // OPTIMIZATION: Use cached string length if available
3116
+ const recordSize = keptRecord.type === 'unchanged' ? keptRecord.line.length + 1 // Use actual line length
3117
+ : JSON.stringify(this.removeTermIdsForSerialization(record)).length + 1;
3118
+ currentOffset += recordSize;
3119
+ }
3120
+ }
3121
+ } else {
3122
+ // Process kept records in their original offset order (no unchanged records)
3123
+ for (const keptRecord of keptRecords) {
3124
+ if (keptRecord.type === 'parsed') {
3125
+ const record = keptRecord.record;
3126
+ if (record && typeof record === 'object' && record.id) {
3127
+ existingRecords.push(record);
3128
+ newOffsets.push(currentOffset);
3129
+ const recordSize = JSON.stringify(this.removeTermIdsForSerialization(record)).length + 1;
3130
+ currentOffset += recordSize;
3131
+ }
3132
+ }
3133
+ }
3134
+ }
3135
+
3136
+ // Update the offsets array to reflect the new record order
3137
+ this.offsets = newOffsets;
3138
+ return existingRecords;
3139
+ }
3140
+
3141
+ /**
3142
+ * Flush write buffer
3143
+ */
3144
+ async flush() {
3145
+ return this.operationQueue.enqueue(async () => {
3146
+ this.isInsideOperationQueue = true;
3147
+ try {
3148
+ // CRITICAL FIX: Actually flush the writeBuffer by saving data
3149
+ if (this.writeBuffer.length > 0 || this.shouldSave) {
3150
+ await this._doSave();
3151
+ }
3152
+ return Promise.resolve();
3153
+ } finally {
3154
+ this.isInsideOperationQueue = false;
3155
+ }
3156
+ });
3157
+ }
3158
+
3159
+ /**
3160
+ * Flush insertion buffer (backward compatibility)
3161
+ */
3162
+ async flushInsertionBuffer() {
3163
+ // Flush insertion buffer implementation - save any pending data
3164
+ // Use the same robust flush logic as flush()
3165
+ return this.flush();
3166
+ }
3167
+
3168
+ /**
3169
+ * Get memory usage
3170
+ */
3171
+ getMemoryUsage() {
3172
+ return {
3173
+ offsetsCount: this.offsets.length,
3174
+ writeBufferSize: this.writeBuffer ? this.writeBuffer.length : 0,
3175
+ used: this.writeBuffer.length,
3176
+ total: this.offsets.length + this.writeBuffer.length,
3177
+ percentage: 0
3178
+ };
3179
+ }
3180
+ _hasActualIndexData() {
3181
+ if (!this.indexManager) return false;
3182
+ const data = this.indexManager.index.data;
3183
+ for (const field in data) {
3184
+ const fieldData = data[field];
3185
+ for (const value in fieldData) {
3186
+ const hybridData = fieldData[value];
3187
+ if (hybridData.set && hybridData.set.size > 0) {
3188
+ return true;
3189
+ }
3190
+ }
3191
+ }
3192
+ return false;
3193
+ }
3194
+
3195
+ /**
3196
+ * Locate a record by line number and return its byte range
3197
+ * @param {number} n - Line number
3198
+ * @returns {Array} - [start, end] byte range or undefined
3199
+ */
3200
+ locate(n) {
3201
+ if (this.offsets[n] === undefined) {
3202
+ return undefined; // Record doesn't exist
3203
+ }
3204
+
3205
+ // CRITICAL FIX: Calculate end offset correctly to prevent cross-line reading
3206
+ let end;
3207
+ if (n + 1 < this.offsets.length) {
3208
+ // Use next record's start minus 1 (to exclude newline) as this record's end
3209
+ end = this.offsets[n + 1] - 1;
3210
+ } else {
3211
+ // For the last record, use indexOffset (includes the record but not newline)
3212
+ end = this.indexOffset;
3213
+ }
3214
+ return [this.offsets[n], end];
3215
+ }
3216
+
3217
+ /**
3218
+ * Get ranges for streaming based on line numbers
3219
+ * @param {Array|Set} map - Line numbers to get ranges for
3220
+ * @returns {Array} - Array of range objects {start, end, index}
3221
+ */
3222
+ getRanges(map) {
3223
+ return (map || Array.from(this.offsets.keys())).map(n => {
3224
+ const ret = this.locate(n);
3225
+ if (ret !== undefined) return {
3226
+ start: ret[0],
3227
+ end: ret[1],
3228
+ index: n
3229
+ };
3230
+ }).filter(n => n !== undefined);
3231
+ }
3232
+
3233
+ /**
3234
+ * Walk through records using streaming (real implementation)
3235
+ */
3236
+ walk(_x) {
3237
+ var _this = this;
3238
+ return _wrapAsyncGenerator(function* (criteria, options = {}) {
3239
+ // CRITICAL FIX: Validate state before walk operation to prevent crashes
3240
+ _this.validateState();
3241
+ if (!_this.initialized) yield _awaitAsyncGenerator(_this.init());
3242
+
3243
+ // If no data at all, return empty
3244
+ if (_this.indexOffset === 0 && _this.writeBuffer.length === 0) return;
3245
+ let map;
3246
+ if (!Array.isArray(criteria)) {
3247
+ if (criteria instanceof Set) {
3248
+ map = [...criteria];
3249
+ } else if (criteria && typeof criteria === 'object' && Object.keys(criteria).length > 0) {
3250
+ // Only use indexManager.query if criteria has actual filters
3251
+ map = [..._this.indexManager.query(criteria, options)];
3252
+ } else {
3253
+ // For empty criteria {} or null/undefined, get all records
3254
+ // Use writeBuffer length when indexOffset is 0 (data not saved yet)
3255
+ const totalRecords = _this.indexOffset > 0 ? _this.indexOffset : _this.writeBuffer.length;
3256
+ map = [...Array(totalRecords).keys()];
3257
+ }
3258
+ } else {
3259
+ map = criteria;
3260
+ }
3261
+
3262
+ // Use writeBuffer when available (unsaved data)
3263
+ if (_this.writeBuffer.length > 0) {
3264
+ let count = 0;
3265
+
3266
+ // If map is empty (no index results) but we have criteria, filter writeBuffer directly
3267
+ if (map.length === 0 && criteria && typeof criteria === 'object' && Object.keys(criteria).length > 0) {
3268
+ for (let i = 0; i < _this.writeBuffer.length; i++) {
3269
+ if (options.limit && count >= options.limit) {
3270
+ break;
3271
+ }
3272
+ const entry = _this.writeBuffer[i];
3273
+ if (entry && _this.queryManager.matchesCriteria(entry, criteria, options)) {
3274
+ count++;
3275
+ if (options.includeOffsets) {
3276
+ yield {
3277
+ entry,
3278
+ start: 0,
3279
+ _: i
3280
+ };
3281
+ } else {
3282
+ if (_this.opts.includeLinePosition) {
3283
+ entry._ = i;
3284
+ }
3285
+ yield entry;
3286
+ }
3287
+ }
3288
+ }
3289
+ } else {
3290
+ // Use map-based iteration (for all records or indexed results)
3291
+ for (const lineNumber of map) {
3292
+ if (options.limit && count >= options.limit) {
3293
+ break;
3294
+ }
3295
+ if (lineNumber < _this.writeBuffer.length) {
3296
+ const entry = _this.writeBuffer[lineNumber];
3297
+ if (entry) {
3298
+ count++;
3299
+ if (options.includeOffsets) {
3300
+ yield {
3301
+ entry,
3302
+ start: 0,
3303
+ _: lineNumber
3304
+ };
3305
+ } else {
3306
+ if (_this.opts.includeLinePosition) {
3307
+ entry._ = lineNumber;
3308
+ }
3309
+ yield entry;
3310
+ }
3311
+ }
3312
+ }
3313
+ }
3314
+ }
3315
+ return;
3316
+ }
3317
+
3318
+ // If writeBuffer is empty but we have saved data, we need to load it from file
3319
+ if (_this.writeBuffer.length === 0 && _this.indexOffset > 0) {
3320
+ // Load data from file for querying
3321
+ try {
3322
+ let data;
3323
+ let lines;
3324
+
3325
+ // Smart threshold: decide between partial reads vs full read
3326
+ const resultPercentage = map ? map.length / _this.indexOffset * 100 : 100;
3327
+ const threshold = _this.opts.partialReadThreshold || 60; // Default 60% threshold
3328
+
3329
+ // Use partial reads when:
3330
+ // 1. We have specific line numbers from index
3331
+ // 2. Results are below threshold percentage
3332
+ // 3. Database is large enough to benefit from partial reads
3333
+ const shouldUsePartialReads = map && map.length > 0 && resultPercentage < threshold && _this.indexOffset > 100; // Only for databases with >100 records
3334
+
3335
+ if (shouldUsePartialReads) {
3336
+ if (_this.opts.debugMode) {
3337
+ console.log(`🔍 Using PARTIAL READS: ${map.length}/${_this.indexOffset} records (${resultPercentage.toFixed(1)}% < ${threshold}% threshold)`);
3338
+ }
3339
+ // Convert 0-based line numbers to 1-based for readSpecificLines
3340
+ const lineNumbers = map.map(num => num + 1);
3341
+ data = yield _awaitAsyncGenerator(_this.fileHandler.readSpecificLines(lineNumbers));
3342
+ lines = data ? data.split('\n') : [];
3343
+ } else {
3344
+ if (_this.opts.debugMode) {
3345
+ console.log(`🔍 Using STREAMING READ: ${map?.length || 0}/${_this.indexOffset} records (${resultPercentage.toFixed(1)}% >= ${threshold}% threshold or small DB)`);
3346
+ }
3347
+ // Use streaming instead of loading all data in memory
3348
+ // This prevents memory issues with large databases
3349
+ const streamingResults = yield _awaitAsyncGenerator(_this.fileHandler.readWithStreaming(criteria, {
3350
+ limit: options.limit,
3351
+ skip: options.skip
3352
+ }, matchesCriteria, _this.serializer));
3353
+
3354
+ // Process streaming results directly without loading all lines
3355
+ for (const record of streamingResults) {
3356
+ if (options.limit && count >= options.limit) {
3357
+ break;
3358
+ }
3359
+ count++;
3360
+
3361
+ // SPACE OPTIMIZATION: Restore term IDs to terms for user
3362
+ const recordWithTerms = _this.restoreTermIdsAfterDeserialization(record);
3363
+ if (options.includeOffsets) {
3364
+ yield {
3365
+ entry: recordWithTerms,
3366
+ start: 0,
3367
+ _: 0
3368
+ };
3369
+ } else {
3370
+ if (_this.opts.includeLinePosition) {
3371
+ recordWithTerms._ = 0;
3372
+ }
3373
+ yield recordWithTerms;
3374
+ }
3375
+ }
3376
+ return; // Exit early since we processed streaming results
3377
+ }
3378
+ if (lines.length > 0) {
3379
+ const records = [];
3380
+ for (const line of lines) {
3381
+ if (line.trim()) {
3382
+ try {
3383
+ // CRITICAL FIX: Use serializer.deserialize instead of JSON.parse to handle array format
3384
+ const record = _this.serializer.deserialize(line);
3385
+ // SPACE OPTIMIZATION: Restore term IDs to terms for user
3386
+ const recordWithTerms = _this.restoreTermIdsAfterDeserialization(record);
3387
+ records.push(recordWithTerms);
3388
+ } catch (error) {
3389
+ // Skip invalid lines
3390
+ }
3391
+ }
3392
+ }
3393
+
3394
+ // Use loaded records for querying
3395
+ let count = 0;
3396
+
3397
+ // When using partial reads, records correspond to the requested line numbers
3398
+ if (shouldUsePartialReads) {
3399
+ for (let i = 0; i < Math.min(records.length, map.length); i++) {
3400
+ if (options.limit && count >= options.limit) {
3401
+ break;
3402
+ }
3403
+ const entry = records[i];
3404
+ const lineNumber = map[i];
3405
+ if (entry) {
3406
+ count++;
3407
+ if (options.includeOffsets) {
3408
+ yield {
3409
+ entry,
3410
+ start: 0,
3411
+ _: lineNumber
3412
+ };
3413
+ } else {
3414
+ if (_this.opts.includeLinePosition) {
3415
+ entry._ = lineNumber;
3416
+ }
3417
+ yield entry;
3418
+ }
3419
+ }
3420
+ }
3421
+ } else {
3422
+ // Fallback to original logic when reading all data
3423
+ for (const lineNumber of map) {
3424
+ if (options.limit && count >= options.limit) {
3425
+ break;
3426
+ }
3427
+ if (lineNumber < records.length) {
3428
+ const entry = records[lineNumber];
3429
+ if (entry) {
3430
+ count++;
3431
+ if (options.includeOffsets) {
3432
+ yield {
3433
+ entry,
3434
+ start: 0,
3435
+ _: lineNumber
3436
+ };
3437
+ } else {
3438
+ if (_this.opts.includeLinePosition) {
3439
+ entry._ = lineNumber;
3440
+ }
3441
+ yield entry;
3442
+ }
3443
+ }
3444
+ }
3445
+ }
3446
+ }
3447
+ return;
3448
+ }
3449
+ } catch (error) {
3450
+ // If file reading fails, continue to file-based streaming
3451
+ }
3452
+ }
3453
+
3454
+ // Use file-based streaming for saved data
3455
+ const ranges = _this.getRanges(map);
3456
+ const groupedRanges = yield _awaitAsyncGenerator(_this.fileHandler.groupedRanges(ranges));
3457
+ const fd = yield _awaitAsyncGenerator(_fs.default.promises.open(_this.fileHandler.file, 'r'));
3458
+ try {
3459
+ let count = 0;
3460
+ for (const groupedRange of groupedRanges) {
3461
+ if (options.limit && count >= options.limit) {
3462
+ break;
3463
+ }
3464
+ var _iteratorAbruptCompletion = false;
3465
+ var _didIteratorError = false;
3466
+ var _iteratorError;
3467
+ try {
3468
+ for (var _iterator = _asyncIterator(_this.fileHandler.readGroupedRange(groupedRange, fd)), _step; _iteratorAbruptCompletion = !(_step = yield _awaitAsyncGenerator(_iterator.next())).done; _iteratorAbruptCompletion = false) {
3469
+ const row = _step.value;
3470
+ {
3471
+ if (options.limit && count >= options.limit) {
3472
+ break;
3473
+ }
3474
+ const entry = yield _awaitAsyncGenerator(_this.serializer.deserialize(row.line, {
3475
+ compress: _this.opts.compress,
3476
+ v8: _this.opts.v8
3477
+ }));
3478
+ if (entry === null) continue;
3479
+
3480
+ // SPACE OPTIMIZATION: Restore term IDs to terms for user
3481
+ const entryWithTerms = _this.restoreTermIdsAfterDeserialization(entry);
3482
+ count++;
3483
+ if (options.includeOffsets) {
3484
+ yield {
3485
+ entry: entryWithTerms,
3486
+ start: row.start,
3487
+ _: row._ || _this.offsets.findIndex(n => n === row.start)
3488
+ };
3489
+ } else {
3490
+ if (_this.opts.includeLinePosition) {
3491
+ entryWithTerms._ = row._ || _this.offsets.findIndex(n => n === row.start);
3492
+ }
3493
+ yield entryWithTerms;
3494
+ }
3495
+ }
3496
+ }
3497
+ } catch (err) {
3498
+ _didIteratorError = true;
3499
+ _iteratorError = err;
3500
+ } finally {
3501
+ try {
3502
+ if (_iteratorAbruptCompletion && _iterator.return != null) {
3503
+ yield _awaitAsyncGenerator(_iterator.return());
3504
+ }
3505
+ } finally {
3506
+ if (_didIteratorError) {
3507
+ throw _iteratorError;
3508
+ }
3509
+ }
3510
+ }
3511
+ }
3512
+ } finally {
3513
+ yield _awaitAsyncGenerator(fd.close());
3514
+ }
3515
+ }).apply(this, arguments);
3516
+ }
3517
+
3518
+ /**
3519
+ * Iterate through records with bulk update capabilities
3520
+ * Allows in-place modifications and deletions with optimized performance
3521
+ *
3522
+ * @param {Object} criteria - Query criteria
3523
+ * @param {Object} options - Iteration options
3524
+ * @param {number} options.chunkSize - Batch size for processing (default: 1000)
3525
+ * @param {string} options.strategy - Processing strategy: 'streaming' (always uses walk() method)
3526
+ * @param {boolean} options.autoSave - Auto-save after each chunk (default: false)
3527
+ * @param {Function} options.progressCallback - Progress callback function
3528
+ * @param {boolean} options.detectChanges - Auto-detect changes (default: true)
3529
+ * @returns {AsyncGenerator} Generator yielding records for modification
3530
+ */
3531
+ iterate(_x2) {
3532
+ var _this2 = this;
3533
+ return _wrapAsyncGenerator(function* (criteria, options = {}) {
3534
+ // CRITICAL FIX: Validate state before iterate operation
3535
+ _this2.validateState();
3536
+ if (!_this2.initialized) yield _awaitAsyncGenerator(_this2.init());
3537
+
3538
+ // Set default options
3539
+ const opts = {
3540
+ chunkSize: 1000,
3541
+ strategy: 'streaming',
3542
+ // Always use walk() method for optimal performance
3543
+ autoSave: false,
3544
+ detectChanges: true,
3545
+ ...options
3546
+ };
3547
+
3548
+ // If no data, return empty
3549
+ if (_this2.indexOffset === 0 && _this2.writeBuffer.length === 0) return;
3550
+ const startTime = Date.now();
3551
+ let processedCount = 0;
3552
+ let modifiedCount = 0;
3553
+ let deletedCount = 0;
3554
+
3555
+ // Buffers for batch processing
3556
+ const updateBuffer = [];
3557
+ const deleteBuffer = new Set();
3558
+ const originalRecords = new Map(); // Track original records for change detection
3559
+
3560
+ try {
3561
+ // Always use walk() now that the bug is fixed - it works for both small and large datasets
3562
+ var _iteratorAbruptCompletion2 = false;
3563
+ var _didIteratorError2 = false;
3564
+ var _iteratorError2;
3565
+ try {
3566
+ for (var _iterator2 = _asyncIterator(_this2.walk(criteria, options)), _step2; _iteratorAbruptCompletion2 = !(_step2 = yield _awaitAsyncGenerator(_iterator2.next())).done; _iteratorAbruptCompletion2 = false) {
3567
+ const entry = _step2.value;
3568
+ {
3569
+ processedCount++;
3570
+
3571
+ // Store original record for change detection BEFORE yielding
3572
+ let originalRecord = null;
3573
+ if (opts.detectChanges) {
3574
+ originalRecord = _this2._createShallowCopy(entry);
3575
+ originalRecords.set(entry.id, originalRecord);
3576
+ }
3577
+
3578
+ // Create wrapper based on performance preference
3579
+ const entryWrapper = opts.highPerformance ? _this2._createHighPerformanceWrapper(entry, originalRecord) : _this2._createEntryProxy(entry, originalRecord);
3580
+
3581
+ // Yield the wrapper for user modification
3582
+ yield entryWrapper;
3583
+
3584
+ // Check if entry was modified or deleted AFTER yielding
3585
+ if (entryWrapper.isMarkedForDeletion) {
3586
+ // Entry was marked for deletion
3587
+ if (originalRecord) {
3588
+ deleteBuffer.add(originalRecord.id);
3589
+ deletedCount++;
3590
+ }
3591
+ } else if (opts.detectChanges && originalRecord) {
3592
+ // Check if entry was modified by comparing with original (optimized comparison)
3593
+ if (_this2._hasRecordChanged(entry, originalRecord)) {
3594
+ updateBuffer.push(entry);
3595
+ modifiedCount++;
3596
+ }
3597
+ } else if (entryWrapper.isModified) {
3598
+ // Manual change detection
3599
+ updateBuffer.push(entry);
3600
+ modifiedCount++;
3601
+ }
3602
+
3603
+ // Process batch when chunk size is reached
3604
+ if (updateBuffer.length >= opts.chunkSize || deleteBuffer.size >= opts.chunkSize) {
3605
+ yield _awaitAsyncGenerator(_this2._processIterateBatch(updateBuffer, deleteBuffer, opts));
3606
+
3607
+ // Clear buffers
3608
+ updateBuffer.length = 0;
3609
+ deleteBuffer.clear();
3610
+ originalRecords.clear();
3611
+
3612
+ // Progress callback
3613
+ if (opts.progressCallback) {
3614
+ opts.progressCallback({
3615
+ processed: processedCount,
3616
+ modified: modifiedCount,
3617
+ deleted: deletedCount,
3618
+ elapsed: Date.now() - startTime
3619
+ });
3620
+ }
3621
+ }
3622
+ }
3623
+ }
3624
+
3625
+ // Process remaining records in buffers
3626
+ } catch (err) {
3627
+ _didIteratorError2 = true;
3628
+ _iteratorError2 = err;
3629
+ } finally {
3630
+ try {
3631
+ if (_iteratorAbruptCompletion2 && _iterator2.return != null) {
3632
+ yield _awaitAsyncGenerator(_iterator2.return());
3633
+ }
3634
+ } finally {
3635
+ if (_didIteratorError2) {
3636
+ throw _iteratorError2;
3637
+ }
3638
+ }
3639
+ }
3640
+ if (updateBuffer.length > 0 || deleteBuffer.size > 0) {
3641
+ yield _awaitAsyncGenerator(_this2._processIterateBatch(updateBuffer, deleteBuffer, opts));
3642
+ }
3643
+
3644
+ // Final progress callback (always called)
3645
+ if (opts.progressCallback) {
3646
+ opts.progressCallback({
3647
+ processed: processedCount,
3648
+ modified: modifiedCount,
3649
+ deleted: deletedCount,
3650
+ elapsed: Date.now() - startTime,
3651
+ completed: true
3652
+ });
3653
+ }
3654
+ if (_this2.opts.debugMode) {
3655
+ console.log(`🔄 ITERATE COMPLETED: ${processedCount} processed, ${modifiedCount} modified, ${deletedCount} deleted in ${Date.now() - startTime}ms`);
3656
+ }
3657
+ } catch (error) {
3658
+ console.error('Iterate operation failed:', error);
3659
+ throw error;
3660
+ }
3661
+ }).apply(this, arguments);
3662
+ }
3663
+
3664
+ /**
3665
+ * Process a batch of updates and deletes from iterate operation
3666
+ * @private
3667
+ */
3668
+ async _processIterateBatch(updateBuffer, deleteBuffer, options) {
3669
+ if (updateBuffer.length === 0 && deleteBuffer.size === 0) return;
3670
+ const startTime = Date.now();
3671
+ try {
3672
+ // Process updates
3673
+ if (updateBuffer.length > 0) {
3674
+ for (const record of updateBuffer) {
3675
+ // Remove the _modified flag if it exists
3676
+ delete record._modified;
3677
+
3678
+ // Update record in writeBuffer or add to writeBuffer
3679
+ const index = this.writeBuffer.findIndex(r => r.id === record.id);
3680
+ if (index !== -1) {
3681
+ // Record is already in writeBuffer, update it
3682
+ this.writeBuffer[index] = record;
3683
+ } else {
3684
+ // Record is in file, add updated version to writeBuffer
3685
+ this.writeBuffer.push(record);
3686
+ }
3687
+
3688
+ // Update index
3689
+ await this.indexManager.update(record, record, this.writeBuffer.length - 1);
3690
+ }
3691
+ if (this.opts.debugMode) {
3692
+ console.log(`🔄 ITERATE: Updated ${updateBuffer.length} records in ${Date.now() - startTime}ms`);
3693
+ }
3694
+ }
3695
+
3696
+ // Process deletes
3697
+ if (deleteBuffer.size > 0) {
3698
+ for (const recordId of deleteBuffer) {
3699
+ // Find the record to get its data for term mapping removal
3700
+ const record = this.writeBuffer.find(r => r.id === recordId) || (await this.findOne({
3701
+ id: recordId
3702
+ }));
3703
+ if (record) {
3704
+ // Remove term mapping
3705
+ this.removeTermMapping(record);
3706
+
3707
+ // Remove from index
3708
+ await this.indexManager.remove(record);
3709
+
3710
+ // Remove from writeBuffer or mark as deleted
3711
+ const index = this.writeBuffer.findIndex(r => r.id === recordId);
3712
+ if (index !== -1) {
3713
+ this.writeBuffer.splice(index, 1);
3714
+ } else {
3715
+ // Mark as deleted if not in writeBuffer
3716
+ this.deletedIds.add(recordId);
3717
+ }
3718
+ }
3719
+ }
3720
+ if (this.opts.debugMode) {
3721
+ console.log(`🗑️ ITERATE: Deleted ${deleteBuffer.size} records in ${Date.now() - startTime}ms`);
3722
+ }
3723
+ }
3724
+
3725
+ // Auto-save if enabled
3726
+ if (options.autoSave) {
3727
+ await this.save();
3728
+ }
3729
+ this.shouldSave = true;
3730
+ this.performanceStats.operations++;
3731
+ } catch (error) {
3732
+ console.error('Batch processing failed:', error);
3733
+ throw error;
3734
+ }
3735
+ }
3736
+
3737
+ /**
3738
+ * Close the database
3739
+ */
3740
+ async close() {
3741
+ if (this.destroyed || this.closed) return;
3742
+ try {
3743
+ if (this.opts.debugMode) {
3744
+ console.log(`💾 close(): Saving and closing database (reopenable)`);
3745
+ }
3746
+
3747
+ // 1. Save all pending data and index data to files
3748
+ if (this.writeBuffer.length > 0 || this.shouldSave) {
3749
+ await this.save();
3750
+ // Ensure writeBuffer is cleared after save
3751
+ if (this.writeBuffer.length > 0) {
3752
+ console.warn('⚠️ WriteBuffer not cleared after save() - forcing clear');
3753
+ this.writeBuffer = [];
3754
+ this.writeBufferOffsets = [];
3755
+ this.writeBufferSizes = [];
3756
+ }
3757
+ } else {
3758
+ // Even if no data to save, ensure index data is persisted
3759
+ await this._saveIndexDataToFile();
3760
+ }
3761
+
3762
+ // 2. Mark as closed (but not destroyed) to allow reopening
3763
+ this.closed = true;
3764
+ this.initialized = false;
3765
+
3766
+ // 3. Clear any remaining state for clean reopening
3767
+ this.writeBuffer = [];
3768
+ this.writeBufferOffsets = [];
3769
+ this.writeBufferSizes = [];
3770
+ this.shouldSave = false;
3771
+ this.isSaving = false;
3772
+ this.lastSaveTime = null;
3773
+ if (this.opts.debugMode) {
3774
+ console.log(`💾 Database closed (can be reopened with init())`);
3775
+ }
3776
+ } catch (error) {
3777
+ console.error('Failed to close database:', error);
3778
+ // Mark as closed even if save failed
3779
+ this.closed = true;
3780
+ this.initialized = false;
3781
+ throw error;
3782
+ }
3783
+ }
3784
+
3785
+ /**
3786
+ * Save index data to .idx.jdb file
3787
+ * @private
3788
+ */
3789
+ async _saveIndexDataToFile() {
3790
+ if (this.indexManager) {
3791
+ try {
3792
+ const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
3793
+ const indexData = {
3794
+ index: this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0 ? this.indexManager.toJSON() : {},
3795
+ offsets: this.offsets,
3796
+ // Save actual offsets for efficient file operations
3797
+ indexOffset: this.indexOffset,
3798
+ // Save file size for proper range calculations
3799
+ // Save configuration for reuse when database exists
3800
+ config: {
3801
+ fields: this.opts.fields,
3802
+ indexes: this.opts.indexes,
3803
+ originalIndexes: this.opts.originalIndexes,
3804
+ schema: this.serializer?.getSchema?.() || null
3805
+ }
3806
+ };
3807
+
3808
+ // Include term mapping data in .idx file if term mapping fields exist
3809
+ const termMappingFields = this.getTermMappingFields();
3810
+ if (termMappingFields.length > 0 && this.termManager) {
3811
+ const termData = await this.termManager.saveTerms();
3812
+ indexData.termMapping = termData;
3813
+ }
3814
+
3815
+ // Always create .idx file for databases with indexes, even if empty
3816
+ // This ensures the database structure is complete
3817
+ const originalFile = this.fileHandler.file;
3818
+ this.fileHandler.file = idxPath;
3819
+ await this.fileHandler.writeAll(JSON.stringify(indexData, null, 2));
3820
+ this.fileHandler.file = originalFile;
3821
+ if (this.opts.debugMode) {
3822
+ console.log(`💾 Index data saved to ${idxPath}`);
3823
+ }
3824
+ } catch (error) {
3825
+ console.warn('Failed to save index data:', error.message);
3826
+ throw error; // Re-throw to let caller handle
3827
+ }
3828
+ }
3829
+ }
3830
+
3831
+ /**
3832
+ * Get operation queue statistics
3833
+ */
3834
+ getQueueStats() {
3835
+ if (!this.operationQueue) {
3836
+ return {
3837
+ queueLength: 0,
3838
+ isProcessing: false,
3839
+ totalOperations: 0,
3840
+ completedOperations: 0,
3841
+ failedOperations: 0,
3842
+ successRate: 0,
3843
+ averageProcessingTime: 0,
3844
+ maxProcessingTime: 0
3845
+ };
3846
+ }
3847
+ return this.operationQueue.getStats();
3848
+ }
3849
+
3850
+ /**
3851
+ * Wait for all pending operations to complete
3852
+ * This includes operation queue AND active insert sessions
3853
+ * If called with no arguments, interpret as waitForOperations(null).
3854
+ * If argument provided (maxWaitTime), pass that on.
3855
+ */
3856
+ async waitForOperations(maxWaitTime = null) {
3857
+ // Accept any falsy/undefined/empty call as "wait for all"
3858
+ const actualWaitTime = arguments.length === 0 ? null : maxWaitTime;
3859
+ const startTime = Date.now();
3860
+ const hasTimeout = actualWaitTime !== null && actualWaitTime !== undefined;
3861
+
3862
+ // Wait for operation queue
3863
+ if (this.operationQueue) {
3864
+ const queueCompleted = await this.operationQueue.waitForCompletion(actualWaitTime);
3865
+ if (!queueCompleted && hasTimeout) {
3866
+ return false;
3867
+ }
3868
+ }
3869
+
3870
+ // Wait for active insert sessions
3871
+ if (this.activeInsertSessions.size > 0) {
3872
+ if (this.opts.debugMode) {
3873
+ console.log(`⏳ waitForOperations: Waiting for ${this.activeInsertSessions.size} active insert sessions`);
3874
+ }
3875
+
3876
+ // Wait for all active sessions to complete
3877
+ const sessionPromises = Array.from(this.activeInsertSessions).map(session => session.waitForOperations(actualWaitTime));
3878
+ try {
3879
+ const sessionResults = await Promise.all(sessionPromises);
3880
+
3881
+ // Check if any session timed out
3882
+ if (hasTimeout && sessionResults.some(result => !result)) {
3883
+ return false;
3884
+ }
3885
+ } catch (error) {
3886
+ if (this.opts.debugMode) {
3887
+ console.log(`⚠️ waitForOperations: Error waiting for sessions: ${error.message}`);
3888
+ }
3889
+ // Continue anyway - don't fail the entire operation
3890
+ }
3891
+ }
3892
+ return true;
3893
+ }
3894
+ }
3895
+ exports.Database = Database;
3896
+ var _default = exports.default = Database;