raptiye 0.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 (41) hide show
  1. package/README.md +171 -0
  2. package/bench/bench-batch-curve.js +121 -0
  3. package/bench/bench-event-loop.js +91 -0
  4. package/bench/bench-failover.js +172 -0
  5. package/bench/bench-memory.js +107 -0
  6. package/bench/bench-partition.js +134 -0
  7. package/bench/bench-pipeline.js +94 -0
  8. package/bench/bench-recovery.js +134 -0
  9. package/bench/bench-replication.js +107 -0
  10. package/bench/bench-single-node.js +77 -0
  11. package/bench/bench-slow-follower.js +96 -0
  12. package/bench/harness.js +87 -0
  13. package/bench/run-all.js +83 -0
  14. package/bench-results.json +598 -0
  15. package/index.js +30 -0
  16. package/package.json +24 -0
  17. package/src/core/engine.js +907 -0
  18. package/src/core/invariants.js +146 -0
  19. package/src/node/raptiye.js +296 -0
  20. package/src/node/stats.js +82 -0
  21. package/src/protocol/checksum.js +66 -0
  22. package/src/protocol/wire.js +551 -0
  23. package/src/replication/pipeline.js +235 -0
  24. package/src/sim/cluster.js +316 -0
  25. package/src/sim/prng.js +52 -0
  26. package/src/sim/virtual-clock.js +82 -0
  27. package/src/storage/file-log.js +394 -0
  28. package/src/storage/interface.js +113 -0
  29. package/src/storage/memory-log.js +183 -0
  30. package/src/transport/interface.js +47 -0
  31. package/src/transport/memory-transport.js +67 -0
  32. package/src/transport/tcp-transport.js +200 -0
  33. package/src/types.js +87 -0
  34. package/test/chaos/chaos.test.js +112 -0
  35. package/test/integration/cluster.test.js +192 -0
  36. package/test/integration/node.test.js +78 -0
  37. package/test/integration/replication-pipeline.test.js +45 -0
  38. package/test/unit/core.test.js +163 -0
  39. package/test/unit/protocol.test.js +181 -0
  40. package/test/unit/storage.test.js +127 -0
  41. package/test/unit/transport.test.js +90 -0
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Durable, crash-safe Write-Ahead Log (WAL) storage for Raptiye.
3
+ * Uses append-only log format with CRC32 integrity checks and atomic state barriers.
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { LogStorage } from './interface.js';
9
+ import { crc32 } from '../protocol/checksum.js';
10
+
11
+ const WAL_MAGIC = 0x574C; // 'WL'
12
+ const WAL_RECORD_HEADER_SIZE = 27; // 2 magic + 4 crc + 8 term + 8 index + 1 type + 4 payloadLen
13
+
14
+ export class FileLog extends LogStorage {
15
+ constructor(dataDir) {
16
+ super();
17
+ this.dataDir = dataDir;
18
+ this.walPath = path.join(dataDir, 'wal.log');
19
+ this.statePath = path.join(dataDir, 'hard_state.bin');
20
+ this.snapPath = path.join(dataDir, 'snapshot.bin');
21
+
22
+ this._fd = null;
23
+ this._entries = [];
24
+ this._snapshot = null;
25
+ this._snapshotIndex = 0n;
26
+ this._snapshotTerm = 0n;
27
+
28
+ this._hardState = {
29
+ currentTerm: 0n,
30
+ votedFor: null
31
+ };
32
+
33
+ this._init();
34
+ }
35
+
36
+ _init() {
37
+ if (!fs.existsSync(this.dataDir)) {
38
+ fs.mkdirSync(this.dataDir, { recursive: true });
39
+ }
40
+
41
+ this._loadHardState();
42
+ this._loadSnapshot();
43
+ this._openAndRecoverWal();
44
+ }
45
+
46
+ _loadHardState() {
47
+ if (fs.existsSync(this.statePath)) {
48
+ const buf = fs.readFileSync(this.statePath);
49
+ if (buf.length >= 10) {
50
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
51
+ this._hardState.currentTerm = view.getBigUint64(0, false);
52
+ const votedForRaw = view.getUint16(8, false);
53
+ this._hardState.votedFor = votedForRaw === 0xFFFF ? null : votedForRaw;
54
+ }
55
+ }
56
+ }
57
+
58
+ _saveHardState() {
59
+ const buf = new Uint8Array(10);
60
+ const view = new DataView(buf.buffer, buf.byteOffset, 10);
61
+ view.setBigUint64(0, this._hardState.currentTerm, false);
62
+ view.setUint16(8, this._hardState.votedFor === null ? 0xFFFF : this._hardState.votedFor, false);
63
+
64
+ const tmpPath = `${this.statePath}.tmp`;
65
+ const tmpFd = fs.openSync(tmpPath, 'w');
66
+ fs.writeSync(tmpFd, buf);
67
+ fs.fsyncSync(tmpFd);
68
+ fs.closeSync(tmpFd);
69
+
70
+ fs.renameSync(tmpPath, this.statePath);
71
+ }
72
+
73
+ _loadSnapshot() {
74
+ if (fs.existsSync(this.snapPath)) {
75
+ const buf = fs.readFileSync(this.snapPath);
76
+ if (buf.length >= 16) {
77
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
78
+ const index = view.getBigUint64(0, false);
79
+ const term = view.getBigUint64(8, false);
80
+ const data = buf.subarray(16);
81
+ this._snapshot = { index, term, data };
82
+ this._snapshotIndex = index;
83
+ this._snapshotTerm = term;
84
+ }
85
+ }
86
+ }
87
+
88
+ _openAndRecoverWal() {
89
+ const exists = fs.existsSync(this.walPath);
90
+ this._fd = fs.openSync(this.walPath, exists ? 'r+' : 'w+');
91
+
92
+ if (!exists) return;
93
+
94
+ const stats = fs.fstatSync(this._fd);
95
+ const fileSize = stats.size;
96
+ if (fileSize === 0) return;
97
+
98
+ const fileBuf = fs.readFileSync(this.walPath);
99
+ let offset = 0;
100
+ let validEnd = 0;
101
+
102
+ while (offset + WAL_RECORD_HEADER_SIZE <= fileSize) {
103
+ const view = new DataView(fileBuf.buffer, fileBuf.byteOffset + offset, fileBuf.byteLength - offset);
104
+ const magic = view.getUint16(0, false);
105
+ if (magic !== WAL_MAGIC) break;
106
+
107
+ const recordCrc = view.getUint32(2, false);
108
+ const term = view.getBigUint64(6, false);
109
+ const index = view.getBigUint64(14, false);
110
+ const type = view.getUint8(22);
111
+ const payloadLen = view.getUint32(23, false);
112
+
113
+ if (offset + WAL_RECORD_HEADER_SIZE + payloadLen > fileSize) {
114
+ // Partial record at EOF
115
+ break;
116
+ }
117
+
118
+ const payloadOffset = offset + WAL_RECORD_HEADER_SIZE;
119
+ // Verify CRC over [term, index, type, payloadLen, payload]
120
+ const checkedSlice = fileBuf.subarray(offset + 6, payloadOffset + payloadLen);
121
+ const computedCrc = crc32(checkedSlice);
122
+
123
+ if (computedCrc !== recordCrc) {
124
+ // Corrupt record, stop recovery here
125
+ break;
126
+ }
127
+
128
+ const payload = new Uint8Array(payloadLen);
129
+ payload.set(fileBuf.subarray(payloadOffset, payloadOffset + payloadLen));
130
+
131
+ this._entries.push({
132
+ term,
133
+ index,
134
+ type,
135
+ payload,
136
+ checksum: recordCrc,
137
+ fileOffset: offset,
138
+ totalRecordSize: WAL_RECORD_HEADER_SIZE + payloadLen
139
+ });
140
+
141
+ offset += WAL_RECORD_HEADER_SIZE + payloadLen;
142
+ validEnd = offset;
143
+ }
144
+
145
+ if (validEnd < fileSize) {
146
+ // Truncate partial or corrupt tail
147
+ fs.ftruncateSync(this._fd, validEnd);
148
+ }
149
+ }
150
+
151
+ append(entry) {
152
+ const term = BigInt(entry.term);
153
+ const index = BigInt(entry.index);
154
+ const payload = entry.payload || new Uint8Array(0);
155
+ const payloadLen = payload.byteLength;
156
+
157
+ const recordSize = WAL_RECORD_HEADER_SIZE + payloadLen;
158
+ const buf = new Uint8Array(recordSize);
159
+ const view = new DataView(buf.buffer, buf.byteOffset, recordSize);
160
+
161
+ view.setUint16(0, WAL_MAGIC, false);
162
+ view.setBigUint64(6, term, false);
163
+ view.setBigUint64(14, index, false);
164
+ view.setUint8(22, entry.type || 1);
165
+ view.setUint32(23, payloadLen, false);
166
+
167
+ if (payloadLen > 0) {
168
+ buf.set(payload, WAL_RECORD_HEADER_SIZE);
169
+ }
170
+
171
+ const checkedSlice = buf.subarray(6, recordSize);
172
+ const recordCrc = crc32(checkedSlice);
173
+ view.setUint32(2, recordCrc, false);
174
+
175
+ // Synchronously write to WAL file
176
+ const stats = fs.fstatSync(this._fd);
177
+ const fileOffset = stats.size;
178
+ fs.writeSync(this._fd, buf, 0, recordSize, fileOffset);
179
+
180
+ this._entries.push({
181
+ term,
182
+ index,
183
+ type: entry.type || 1,
184
+ payload,
185
+ checksum: recordCrc,
186
+ fileOffset,
187
+ totalRecordSize: recordSize
188
+ });
189
+ }
190
+
191
+ appendMany(entries) {
192
+ for (let i = 0; i < entries.length; i++) {
193
+ this.append(entries[i]);
194
+ }
195
+ }
196
+
197
+ sync() {
198
+ if (this._fd !== null) {
199
+ fs.fsyncSync(this._fd);
200
+ }
201
+ }
202
+
203
+ entries(fromIndex, toIndex, maxBytes = Infinity) {
204
+ const from = BigInt(fromIndex);
205
+ const to = BigInt(toIndex);
206
+ const first = this.firstIndex();
207
+ const last = this.lastIndex();
208
+
209
+ if (from > to || from > last || to < first) {
210
+ return [];
211
+ }
212
+
213
+ const startIdx = from < first ? 0 : Number(from - first);
214
+ const endIdx = to > last ? this._entries.length - 1 : Number(to - first);
215
+
216
+ const result = [];
217
+ let accBytes = 0;
218
+
219
+ for (let i = startIdx; i <= endIdx; i++) {
220
+ const e = this._entries[i];
221
+ const entryBytes = e.payload.byteLength + 25;
222
+ if (result.length > 0 && accBytes + entryBytes > maxBytes) {
223
+ break;
224
+ }
225
+ result.push(e);
226
+ accBytes += entryBytes;
227
+ }
228
+
229
+ return result;
230
+ }
231
+
232
+ term(index) {
233
+ const idx = BigInt(index);
234
+ if (idx === 0n) return 0n;
235
+ if (idx === this._snapshotIndex) return this._snapshotTerm;
236
+ if (idx < this.firstIndex()) return null;
237
+ if (idx > this.lastIndex()) return null;
238
+
239
+ const offset = Number(idx - this.firstIndex());
240
+ return this._entries[offset].term;
241
+ }
242
+
243
+ lastIndex() {
244
+ if (this._entries.length === 0) {
245
+ return this._snapshotIndex;
246
+ }
247
+ return this._entries[this._entries.length - 1].index;
248
+ }
249
+
250
+ lastTerm() {
251
+ if (this._entries.length === 0) {
252
+ return this._snapshotTerm;
253
+ }
254
+ return this._entries[this._entries.length - 1].term;
255
+ }
256
+
257
+ firstIndex() {
258
+ return this._snapshotIndex + 1n;
259
+ }
260
+
261
+ truncateFrom(index) {
262
+ const idx = BigInt(index);
263
+ const first = this.firstIndex();
264
+
265
+ if (idx < first) {
266
+ throw new Error(`Cannot truncate log prior to first retained index: ${idx} < ${first}`);
267
+ }
268
+ if (idx > this.lastIndex()) {
269
+ return;
270
+ }
271
+
272
+ const keepCount = Number(idx - first);
273
+ let truncateOffset = 0;
274
+ if (keepCount < this._entries.length) {
275
+ truncateOffset = this._entries[keepCount].fileOffset;
276
+ this._entries.length = keepCount;
277
+ fs.ftruncateSync(this._fd, truncateOffset);
278
+ }
279
+ }
280
+
281
+ compactThrough(index) {
282
+ const idx = BigInt(index);
283
+ if (idx <= this._snapshotIndex) {
284
+ return;
285
+ }
286
+
287
+ const last = this.lastIndex();
288
+ if (idx > last) {
289
+ this._snapshotIndex = idx;
290
+ this._entries.length = 0;
291
+ fs.ftruncateSync(this._fd, 0);
292
+ return;
293
+ }
294
+
295
+ const discardCount = Number(idx - this._snapshotIndex);
296
+ const lastDiscarded = this._entries[discardCount - 1];
297
+ this._snapshotTerm = lastDiscarded.term;
298
+ this._snapshotIndex = idx;
299
+ this._entries.splice(0, discardCount);
300
+
301
+ // Rewrite WAL with remaining entries
302
+ this._rewriteWal();
303
+ }
304
+
305
+ _rewriteWal() {
306
+ const tmpWal = `${this.walPath}.tmp`;
307
+ const tmpFd = fs.openSync(tmpWal, 'w');
308
+
309
+ for (let i = 0; i < this._entries.length; i++) {
310
+ const e = this._entries[i];
311
+ const payloadLen = e.payload.byteLength;
312
+ const recordSize = WAL_RECORD_HEADER_SIZE + payloadLen;
313
+ const buf = new Uint8Array(recordSize);
314
+ const view = new DataView(buf.buffer, buf.byteOffset, recordSize);
315
+
316
+ view.setUint16(0, WAL_MAGIC, false);
317
+ view.setBigUint64(6, e.term, false);
318
+ view.setBigUint64(14, e.index, false);
319
+ view.setUint8(22, e.type || 1);
320
+ view.setUint32(23, payloadLen, false);
321
+ if (payloadLen > 0) {
322
+ buf.set(e.payload, WAL_RECORD_HEADER_SIZE);
323
+ }
324
+ const checkedSlice = buf.subarray(6, recordSize);
325
+ const recordCrc = crc32(checkedSlice);
326
+ view.setUint32(2, recordCrc, false);
327
+
328
+ fs.writeSync(tmpFd, buf);
329
+ }
330
+
331
+ fs.fsyncSync(tmpFd);
332
+ fs.closeSync(tmpFd);
333
+ fs.closeSync(this._fd);
334
+ fs.renameSync(tmpWal, this.walPath);
335
+ this._fd = fs.openSync(this.walPath, 'r+');
336
+
337
+ // Update in-memory offsets
338
+ let offset = 0;
339
+ for (let i = 0; i < this._entries.length; i++) {
340
+ this._entries[i].fileOffset = offset;
341
+ offset += this._entries[i].totalRecordSize;
342
+ }
343
+ }
344
+
345
+ getHardState() {
346
+ return {
347
+ currentTerm: this._hardState.currentTerm,
348
+ votedFor: this._hardState.votedFor
349
+ };
350
+ }
351
+
352
+ setHardState({ currentTerm, votedFor }) {
353
+ this._hardState.currentTerm = BigInt(currentTerm);
354
+ this._hardState.votedFor = votedFor !== null && votedFor !== undefined ? Number(votedFor) : null;
355
+ this._saveHardState();
356
+ }
357
+
358
+ getSnapshot() {
359
+ return this._snapshot;
360
+ }
361
+
362
+ saveSnapshot(snapshot) {
363
+ const index = BigInt(snapshot.index);
364
+ const term = BigInt(snapshot.term);
365
+ const data = snapshot.data || new Uint8Array(0);
366
+
367
+ const buf = new Uint8Array(16 + data.byteLength);
368
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
369
+ view.setBigUint64(0, index, false);
370
+ view.setBigUint64(8, term, false);
371
+ if (data.byteLength > 0) {
372
+ buf.set(data, 16);
373
+ }
374
+
375
+ const tmpSnap = `${this.snapPath}.tmp`;
376
+ const tmpFd = fs.openSync(tmpSnap, 'w');
377
+ fs.writeSync(tmpFd, buf);
378
+ fs.fsyncSync(tmpFd);
379
+ fs.closeSync(tmpFd);
380
+ fs.renameSync(tmpSnap, this.snapPath);
381
+
382
+ this._snapshot = { index, term, data };
383
+ this.compactThrough(index);
384
+ this._snapshotIndex = index;
385
+ this._snapshotTerm = term;
386
+ }
387
+
388
+ close() {
389
+ if (this._fd !== null) {
390
+ fs.closeSync(this._fd);
391
+ this._fd = null;
392
+ }
393
+ }
394
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Abstract storage interface for Raptiye replicated log and hard state.
3
+ */
4
+
5
+ export class LogStorage {
6
+ /**
7
+ * Append a single entry.
8
+ * @param {{ term: bigint, index: bigint, type: number, payload: Uint8Array, checksum?: number }} entry
9
+ */
10
+ append(entry) {
11
+ throw new Error('Not implemented');
12
+ }
13
+
14
+ /**
15
+ * Append multiple entries.
16
+ * @param {Array<{ term: bigint, index: bigint, type: number, payload: Uint8Array, checksum?: number }>} entries
17
+ */
18
+ appendMany(entries) {
19
+ throw new Error('Not implemented');
20
+ }
21
+
22
+ /**
23
+ * Retrieve entries in [fromIndex, toIndex] range, optionally bounded by maxBytes.
24
+ * @param {bigint} fromIndex
25
+ * @param {bigint} toIndex
26
+ * @param {number} [maxBytes=Infinity]
27
+ * @returns {Array<{ term: bigint, index: bigint, type: number, payload: Uint8Array, checksum?: number }>}
28
+ */
29
+ entries(fromIndex, toIndex, maxBytes = Infinity) {
30
+ throw new Error('Not implemented');
31
+ }
32
+
33
+ /**
34
+ * Get the term for a specific log index.
35
+ * @param {bigint} index
36
+ * @returns {bigint|null}
37
+ */
38
+ term(index) {
39
+ throw new Error('Not implemented');
40
+ }
41
+
42
+ /**
43
+ * @returns {bigint} Last log index (0n if empty)
44
+ */
45
+ lastIndex() {
46
+ throw new Error('Not implemented');
47
+ }
48
+
49
+ /**
50
+ * @returns {bigint} Last log term (0n if empty)
51
+ */
52
+ lastTerm() {
53
+ throw new Error('Not implemented');
54
+ }
55
+
56
+ /**
57
+ * @returns {bigint} First retained log index (1n initially, or snapshotIndex + 1n)
58
+ */
59
+ firstIndex() {
60
+ throw new Error('Not implemented');
61
+ }
62
+
63
+ /**
64
+ * Truncate log starting from index (deletes all entries >= index).
65
+ * @param {bigint} index
66
+ */
67
+ truncateFrom(index) {
68
+ throw new Error('Not implemented');
69
+ }
70
+
71
+ /**
72
+ * Compact log up through index (discards entries <= index).
73
+ * @param {bigint} index
74
+ */
75
+ compactThrough(index) {
76
+ throw new Error('Not implemented');
77
+ }
78
+
79
+ /**
80
+ * @returns {{ currentTerm: bigint, votedFor: number|null }}
81
+ */
82
+ getHardState() {
83
+ throw new Error('Not implemented');
84
+ }
85
+
86
+ /**
87
+ * Persist hard state.
88
+ * @param {{ currentTerm: bigint, votedFor: number|null }} state
89
+ */
90
+ setHardState(state) {
91
+ throw new Error('Not implemented');
92
+ }
93
+
94
+ /**
95
+ * @returns {{ index: bigint, term: bigint, data: Uint8Array }|null}
96
+ */
97
+ getSnapshot() {
98
+ throw new Error('Not implemented');
99
+ }
100
+
101
+ /**
102
+ * Save snapshot and record metadata.
103
+ * @param {{ index: bigint, term: bigint, data: Uint8Array }} snapshot
104
+ */
105
+ saveSnapshot(snapshot) {
106
+ throw new Error('Not implemented');
107
+ }
108
+
109
+ /**
110
+ * Close storage.
111
+ */
112
+ close() {}
113
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * High-performance, byte-first in-memory replicated log storage for Raptiye.
3
+ */
4
+
5
+ import { LogStorage } from './interface.js';
6
+ import { crc32 } from '../protocol/checksum.js';
7
+
8
+ export class MemoryLog extends LogStorage {
9
+ constructor() {
10
+ super();
11
+ this._entries = [];
12
+ this._snapshot = null;
13
+ this._snapshotIndex = 0n;
14
+ this._snapshotTerm = 0n;
15
+ this._hardState = {
16
+ currentTerm: 0n,
17
+ votedFor: null
18
+ };
19
+ this._byteSize = 0;
20
+ }
21
+
22
+ append(entry) {
23
+ const term = BigInt(entry.term);
24
+ const index = BigInt(entry.index);
25
+ const payload = entry.payload || new Uint8Array(0);
26
+ const checksum = entry.checksum !== undefined ? entry.checksum : crc32(payload);
27
+
28
+ const stored = {
29
+ term,
30
+ index,
31
+ type: entry.type || 1,
32
+ payload,
33
+ checksum
34
+ };
35
+
36
+ this._entries.push(stored);
37
+ this._byteSize += payload.byteLength + 25; // 25 bytes metadata
38
+ }
39
+
40
+ appendMany(entries) {
41
+ for (let i = 0; i < entries.length; i++) {
42
+ this.append(entries[i]);
43
+ }
44
+ }
45
+
46
+ entries(fromIndex, toIndex, maxBytes = Infinity) {
47
+ const from = BigInt(fromIndex);
48
+ const to = BigInt(toIndex);
49
+ const first = this.firstIndex();
50
+ const last = this.lastIndex();
51
+
52
+ if (from > to || from > last || to < first) {
53
+ return [];
54
+ }
55
+
56
+ const startIdx = from < first ? 0 : Number(from - first);
57
+ const endIdx = to > last ? this._entries.length - 1 : Number(to - first);
58
+
59
+ const result = [];
60
+ let accBytes = 0;
61
+
62
+ for (let i = startIdx; i <= endIdx; i++) {
63
+ const e = this._entries[i];
64
+ const entryBytes = e.payload.byteLength + 25;
65
+ if (result.length > 0 && accBytes + entryBytes > maxBytes) {
66
+ break;
67
+ }
68
+ result.push(e);
69
+ accBytes += entryBytes;
70
+ }
71
+
72
+ return result;
73
+ }
74
+
75
+ term(index) {
76
+ const idx = BigInt(index);
77
+ if (idx === 0n) return 0n;
78
+ if (idx === this._snapshotIndex) return this._snapshotTerm;
79
+ if (idx < this.firstIndex()) return null; // Compacted away
80
+ if (idx > this.lastIndex()) return null;
81
+
82
+ const offset = Number(idx - this.firstIndex());
83
+ return this._entries[offset].term;
84
+ }
85
+
86
+ lastIndex() {
87
+ if (this._entries.length === 0) {
88
+ return this._snapshotIndex;
89
+ }
90
+ return this._entries[this._entries.length - 1].index;
91
+ }
92
+
93
+ lastTerm() {
94
+ if (this._entries.length === 0) {
95
+ return this._snapshotTerm;
96
+ }
97
+ return this._entries[this._entries.length - 1].term;
98
+ }
99
+
100
+ firstIndex() {
101
+ return this._snapshotIndex + 1n;
102
+ }
103
+
104
+ truncateFrom(index) {
105
+ const idx = BigInt(index);
106
+ const first = this.firstIndex();
107
+
108
+ if (idx < first) {
109
+ throw new Error(`Cannot truncate log prior to first retained index: ${idx} < ${first}`);
110
+ }
111
+ if (idx > this.lastIndex()) {
112
+ return;
113
+ }
114
+
115
+ const keepCount = Number(idx - first);
116
+ for (let i = keepCount; i < this._entries.length; i++) {
117
+ this._byteSize -= (this._entries[i].payload.byteLength + 25);
118
+ }
119
+ this._entries.length = keepCount;
120
+ }
121
+
122
+ compactThrough(index) {
123
+ const idx = BigInt(index);
124
+ if (idx <= this._snapshotIndex) {
125
+ return;
126
+ }
127
+
128
+ const last = this.lastIndex();
129
+ if (idx > last) {
130
+ // Discard entire log
131
+ this._snapshotIndex = idx;
132
+ this._entries.length = 0;
133
+ this._byteSize = 0;
134
+ return;
135
+ }
136
+
137
+ const discardCount = Number(idx - this._snapshotIndex);
138
+ for (let i = 0; i < discardCount; i++) {
139
+ this._byteSize -= (this._entries[i].payload.byteLength + 25);
140
+ }
141
+ const lastDiscarded = this._entries[discardCount - 1];
142
+ this._snapshotTerm = lastDiscarded.term;
143
+ this._snapshotIndex = idx;
144
+ this._entries.splice(0, discardCount);
145
+ }
146
+
147
+ getHardState() {
148
+ return {
149
+ currentTerm: this._hardState.currentTerm,
150
+ votedFor: this._hardState.votedFor
151
+ };
152
+ }
153
+
154
+ setHardState({ currentTerm, votedFor }) {
155
+ this._hardState.currentTerm = BigInt(currentTerm);
156
+ this._hardState.votedFor = votedFor !== null && votedFor !== undefined ? Number(votedFor) : null;
157
+ }
158
+
159
+ getSnapshot() {
160
+ return this._snapshot;
161
+ }
162
+
163
+ saveSnapshot(snapshot) {
164
+ const index = BigInt(snapshot.index);
165
+ const term = BigInt(snapshot.term);
166
+ this._snapshot = {
167
+ index,
168
+ term,
169
+ data: snapshot.data
170
+ };
171
+ this.compactThrough(index);
172
+ this._snapshotIndex = index;
173
+ this._snapshotTerm = term;
174
+ }
175
+
176
+ totalBytes() {
177
+ return this._byteSize;
178
+ }
179
+
180
+ entryCount() {
181
+ return this._entries.length;
182
+ }
183
+ }