@wowoengine/sawitdb 2.4.0 → 2.5.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.
@@ -0,0 +1,340 @@
1
+ const fs = require('fs');
2
+ const crypto = require('crypto');
3
+
4
+ /**
5
+ * Write-Ahead Logging (WAL) for SawitDB - OPTIMIZED VERSION
6
+ * Redis-level performance with crash safety
7
+ */
8
+ class WAL {
9
+ constructor(dbPath, options = {}) {
10
+ this.dbPath = dbPath;
11
+ this.walPath = `${dbPath}.wal`;
12
+ this.enabled = options.enabled !== false;
13
+ this.syncMode = options.syncMode || 'normal';
14
+ this.checkpointInterval = options.checkpointInterval || 10000; // Increased from 1000
15
+
16
+ // OPTIMIZATION: Larger buffers and async fsync
17
+ this.groupCommitMs = options.groupCommitMs || 10; // Group commits within 10ms
18
+ this.bufferSize = options.bufferSize || 1024 * 1024; // 1MB buffer
19
+
20
+ this.fd = null;
21
+ this.lsn = 0;
22
+ this.operationCount = 0;
23
+ this.pendingOps = [];
24
+ this.writeBuffer = Buffer.allocUnsafe(this.bufferSize);
25
+ this.bufferOffset = 0;
26
+ this.syncTimer = null;
27
+ this.lastSyncTime = Date.now();
28
+
29
+ if (this.enabled) {
30
+ this._init();
31
+ this._startSyncTimer();
32
+ }
33
+ }
34
+
35
+ _init() {
36
+ // 1. Synchronous phase for Startup/Recovery check
37
+ if (fs.existsSync(this.walPath)) {
38
+ const fd = fs.openSync(this.walPath, 'r+');
39
+ this.lsn = this._readLastLSN(fd);
40
+ fs.closeSync(fd);
41
+ } else {
42
+ this.lsn = 0;
43
+ }
44
+
45
+ // 2. Async Write Stream for runtime
46
+ this.stream = fs.createWriteStream(this.walPath, { flags: 'a' });
47
+
48
+ // Capture fd for manual fsync if needed
49
+ this.stream.on('open', (fd) => {
50
+ this.fd = fd;
51
+ });
52
+
53
+ // Handle errors
54
+ this.stream.on('error', (err) => {
55
+ console.error('[WAL] Stream error:', err);
56
+ });
57
+ }
58
+
59
+ // Helper for _init extraction
60
+ _readLastLSN(fd) {
61
+ const stats = fs.fstatSync(fd);
62
+ if (stats.size === 0) return 0;
63
+
64
+ let maxLSN = 0;
65
+ const buffer = Buffer.allocUnsafe(stats.size);
66
+ fs.readSync(fd, buffer, 0, stats.size, 0);
67
+
68
+ let offset = 0;
69
+ while (offset < buffer.length) {
70
+ const magic = buffer.readUInt32LE(offset);
71
+ if (magic !== 0x57414C00) break;
72
+
73
+ const entrySize = buffer.readUInt32LE(offset + 4);
74
+ const lsn = Number(buffer.readBigUInt64LE(offset + 8));
75
+
76
+ if (lsn > maxLSN) maxLSN = lsn;
77
+ offset += entrySize;
78
+ }
79
+
80
+ return maxLSN;
81
+ }
82
+
83
+ /**
84
+ * OPTIMIZED: Async timer-based flush and fsync
85
+ */
86
+ _startSyncTimer() {
87
+ if (this.syncMode === 'off' || this.syncMode === 'full') return;
88
+
89
+ this.syncTimer = setInterval(() => {
90
+ // 1. Write any pending application-side buffer to OS
91
+ if (this.bufferOffset > 0) {
92
+ this._flushBuffer();
93
+ }
94
+
95
+ // 2. Force OS to persist to Disk (fsync)
96
+ // We use the raw fd from the stream
97
+ if (this.fd) {
98
+ fs.fsync(this.fd, (err) => {
99
+ if (err) console.error('[WAL] fsync warning:', err.message);
100
+ });
101
+ }
102
+ }, this.groupCommitMs);
103
+ }
104
+
105
+ logOperation(operation, table, pageId, beforeImage, afterImage) {
106
+ if (!this.enabled) return;
107
+
108
+ this.lsn++;
109
+ this.operationCount++;
110
+
111
+ const opCode = this._getOpCode(operation);
112
+ const entry = this._createLogEntry(this.lsn, opCode, table, pageId, beforeImage, afterImage);
113
+
114
+ if (this.syncMode === 'full') {
115
+ // FULL: Force sync immediately (blocking, safest)
116
+ // We bypass stream buffer for FULL to ensure it hits disk now?
117
+ // Actually, mixing stream and raw sync write is bad.
118
+ // For FULL mode, we just write to stream and sync immediately?
119
+ // Stream write is async.
120
+ // To support 'FULL' (Strict Durability) correctly with Streams, we'd need to write and wait for callback.
121
+ // But logOperation is synchronous in current SawitDB architecture.
122
+ // Trade-off: We write to stream, but we can't block easily without de-optimizing.
123
+ // Fallback for FULL: Use fs.writeSync directly?
124
+ // If we use writeSync, we need a separate FD or ensure stream state is okay.
125
+ // Mixing is risky.
126
+ // Safe approach for FULL: Use _flushBuffer() then explicit sync?
127
+ this._writeToBuffer(entry);
128
+ this._flushBuffer(); // Pushes to stream
129
+ // Force sync on stream fd (might block event loop if we use fsyncSync)
130
+ if (this.fd) {
131
+ try { fs.fsyncSync(this.fd); } catch (e) { }
132
+ }
133
+ } else {
134
+ // NORMAL / OFF
135
+ this._writeToBuffer(entry);
136
+ }
137
+
138
+ // Auto checkpoint (less frequent)
139
+ if (this.operationCount >= this.checkpointInterval) {
140
+ this.checkpoint();
141
+ }
142
+ }
143
+
144
+ _writeToBuffer(entry) {
145
+ // If buffer is full, flush it to stream
146
+ if (this.bufferOffset + entry.length > this.bufferSize) {
147
+ this._flushBuffer();
148
+ }
149
+
150
+ entry.copy(this.writeBuffer, this.bufferOffset);
151
+ this.bufferOffset += entry.length;
152
+ }
153
+
154
+ _flushBuffer() {
155
+ if (this.bufferOffset === 0) return;
156
+
157
+ // WRITE TO ASYNC STREAM
158
+ const chunk = this.writeBuffer.slice(0, this.bufferOffset);
159
+ this.stream.write(chunk);
160
+
161
+ this.bufferOffset = 0;
162
+ this.lastSyncTime = Date.now();
163
+ }
164
+
165
+ _getOpCode(operation) {
166
+ const codes = {
167
+ 'INSERT': 0x01,
168
+ 'UPDATE': 0x02,
169
+ 'DELETE': 0x03,
170
+ 'CREATE_TABLE': 0x04,
171
+ 'DROP_TABLE': 0x05,
172
+ 'CHECKPOINT': 0x06
173
+ };
174
+ return codes[operation] || 0x00;
175
+ }
176
+
177
+ /**
178
+ * OPTIMIZED: Simplified entry format (no checksums for speed)
179
+ */
180
+ _createLogEntry(lsn, opCode, table, pageId, beforeImage, afterImage) {
181
+ const tableNameBuf = Buffer.alloc(32);
182
+ tableNameBuf.write(table);
183
+
184
+ const beforeSize = beforeImage ? beforeImage.length : 0;
185
+ const afterSize = afterImage ? afterImage.length : 0;
186
+
187
+ // OPTIMIZATION: Remove checksum (4 bytes saved, faster)
188
+ const entrySize = 4 + 4 + 8 + 1 + 32 + 4 + 4 + 4 + beforeSize + afterSize;
189
+ const entry = Buffer.allocUnsafe(entrySize);
190
+
191
+ let offset = 0;
192
+ entry.writeUInt32LE(0x57414C00, offset); offset += 4;
193
+ entry.writeUInt32LE(entrySize, offset); offset += 4;
194
+ entry.writeBigUInt64LE(BigInt(lsn), offset); offset += 8;
195
+ entry.writeUInt8(opCode, offset); offset += 1;
196
+ tableNameBuf.copy(entry, offset); offset += 32;
197
+ entry.writeUInt32LE(pageId, offset); offset += 4;
198
+ entry.writeUInt32LE(beforeSize, offset); offset += 4;
199
+ entry.writeUInt32LE(afterSize, offset); offset += 4;
200
+
201
+ if (beforeImage) {
202
+ beforeImage.copy(entry, offset);
203
+ offset += beforeSize;
204
+ }
205
+
206
+ if (afterImage) {
207
+ afterImage.copy(entry, offset);
208
+ }
209
+
210
+ return entry;
211
+ }
212
+
213
+ _writeEntry(entry) {
214
+ if (!this.fd) return;
215
+ fs.writeSync(this.fd, entry);
216
+ }
217
+
218
+ _flushPendingOps() {
219
+ // Flush buffer if any
220
+ this._flushBuffer();
221
+
222
+ // Clear pending ops array
223
+ this.pendingOps = [];
224
+ }
225
+
226
+ checkpoint() {
227
+ if (!this.enabled) return;
228
+
229
+ this._flushPendingOps();
230
+
231
+ const checkpointEntry = this._createLogEntry(
232
+ ++this.lsn,
233
+ this._getOpCode('CHECKPOINT'),
234
+ '',
235
+ 0,
236
+ null,
237
+ null
238
+ );
239
+ this._writeEntry(checkpointEntry);
240
+ if (this.fd) {
241
+ try {
242
+ fs.fsyncSync(this.fd);
243
+ } catch (e) {
244
+ // Ignore invalid fd if stream closed or not ready
245
+ }
246
+ }
247
+
248
+ this.operationCount = 0;
249
+ }
250
+
251
+ recover() {
252
+ if (!this.enabled || !fs.existsSync(this.walPath)) {
253
+ return [];
254
+ }
255
+
256
+ const stats = fs.statSync(this.walPath);
257
+ if (stats.size === 0) return [];
258
+
259
+ const fd = fs.openSync(this.walPath, 'r');
260
+ const buffer = Buffer.allocUnsafe(stats.size);
261
+ fs.readSync(fd, buffer, 0, stats.size, 0);
262
+ fs.closeSync(fd);
263
+
264
+ const operations = [];
265
+ let offset = 0;
266
+
267
+ while (offset < buffer.length) {
268
+ try {
269
+ const magic = buffer.readUInt32LE(offset);
270
+ if (magic !== 0x57414C00) break;
271
+
272
+ const entrySize = buffer.readUInt32LE(offset + 4);
273
+ const lsn = Number(buffer.readBigUInt64LE(offset + 8));
274
+ const opCode = buffer.readUInt8(offset + 16);
275
+ const tableName = buffer.toString('utf8', offset + 17, offset + 49).replace(/\0/g, '');
276
+ const pageId = buffer.readUInt32LE(offset + 49);
277
+ const beforeSize = buffer.readUInt32LE(offset + 53);
278
+ const afterSize = buffer.readUInt32LE(offset + 57);
279
+
280
+ let dataOffset = offset + 61;
281
+ const beforeImage = beforeSize > 0 ? buffer.slice(dataOffset, dataOffset + beforeSize) : null;
282
+ dataOffset += beforeSize;
283
+ const afterImage = afterSize > 0 ? buffer.slice(dataOffset, dataOffset + afterSize) : null;
284
+
285
+ operations.push({
286
+ lsn,
287
+ opCode,
288
+ tableName,
289
+ pageId,
290
+ beforeImage,
291
+ afterImage
292
+ });
293
+
294
+ offset += entrySize;
295
+ } catch (e) {
296
+ break;
297
+ }
298
+ }
299
+
300
+ return operations;
301
+ }
302
+
303
+ truncate() {
304
+ if (!this.enabled) return;
305
+
306
+ this._flushBuffer(); // Flush any pending first (async)
307
+ // Note: fs.truncateSync works on path or fd.
308
+ // With stream, it might be tricky if stream is writing.
309
+ // Best effort:
310
+ try { fs.truncateSync(this.walPath, 0); } catch (e) { }
311
+ this.lsn = 0;
312
+ this.operationCount = 0;
313
+ }
314
+
315
+ close() {
316
+ if (!this.enabled) return;
317
+
318
+ // Stop sync timer
319
+ if (this.syncTimer) {
320
+ clearInterval(this.syncTimer);
321
+ this.syncTimer = null;
322
+ }
323
+
324
+ // Final flush
325
+ this._flushBuffer();
326
+
327
+ if (this.stream) {
328
+ this.stream.end();
329
+ this.stream = null;
330
+ }
331
+
332
+ if (this.syncMode === 'full' && this.fd) {
333
+ try { fs.closeSync(this.fd); } catch (e) { }
334
+ }
335
+
336
+ this.fd = null;
337
+ }
338
+ }
339
+
340
+ module.exports = WAL;