@push.rocks/smartmongo 3.0.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/tsmdb/engine/IndexEngine.d.ts +23 -3
- package/dist_ts/tsmdb/engine/IndexEngine.js +357 -55
- package/dist_ts/tsmdb/engine/QueryPlanner.d.ts +64 -0
- package/dist_ts/tsmdb/engine/QueryPlanner.js +308 -0
- package/dist_ts/tsmdb/engine/SessionEngine.d.ts +117 -0
- package/dist_ts/tsmdb/engine/SessionEngine.js +232 -0
- package/dist_ts/tsmdb/index.d.ts +7 -0
- package/dist_ts/tsmdb/index.js +6 -1
- package/dist_ts/tsmdb/server/CommandRouter.d.ts +36 -0
- package/dist_ts/tsmdb/server/CommandRouter.js +91 -1
- package/dist_ts/tsmdb/server/TsmdbServer.js +3 -1
- package/dist_ts/tsmdb/server/handlers/AdminHandler.js +106 -6
- package/dist_ts/tsmdb/server/handlers/DeleteHandler.js +15 -3
- package/dist_ts/tsmdb/server/handlers/FindHandler.js +44 -14
- package/dist_ts/tsmdb/server/handlers/InsertHandler.js +4 -1
- package/dist_ts/tsmdb/server/handlers/UpdateHandler.js +31 -5
- package/dist_ts/tsmdb/storage/FileStorageAdapter.d.ts +25 -1
- package/dist_ts/tsmdb/storage/FileStorageAdapter.js +75 -6
- package/dist_ts/tsmdb/storage/IStorageAdapter.d.ts +5 -0
- package/dist_ts/tsmdb/storage/MemoryStorageAdapter.d.ts +1 -0
- package/dist_ts/tsmdb/storage/MemoryStorageAdapter.js +12 -1
- package/dist_ts/tsmdb/storage/WAL.d.ts +117 -0
- package/dist_ts/tsmdb/storage/WAL.js +286 -0
- package/dist_ts/tsmdb/utils/checksum.d.ts +30 -0
- package/dist_ts/tsmdb/utils/checksum.js +77 -0
- package/dist_ts/tsmdb/utils/index.d.ts +1 -0
- package/dist_ts/tsmdb/utils/index.js +2 -0
- package/package.json +2 -2
- package/readme.md +140 -17
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/tsmdb/engine/IndexEngine.ts +375 -56
- package/ts/tsmdb/engine/QueryPlanner.ts +393 -0
- package/ts/tsmdb/engine/SessionEngine.ts +292 -0
- package/ts/tsmdb/index.ts +9 -0
- package/ts/tsmdb/server/CommandRouter.ts +109 -0
- package/ts/tsmdb/server/TsmdbServer.ts +3 -0
- package/ts/tsmdb/server/handlers/AdminHandler.ts +110 -5
- package/ts/tsmdb/server/handlers/DeleteHandler.ts +17 -2
- package/ts/tsmdb/server/handlers/FindHandler.ts +42 -13
- package/ts/tsmdb/server/handlers/InsertHandler.ts +6 -0
- package/ts/tsmdb/server/handlers/UpdateHandler.ts +33 -4
- package/ts/tsmdb/storage/FileStorageAdapter.ts +88 -5
- package/ts/tsmdb/storage/IStorageAdapter.ts +6 -0
- package/ts/tsmdb/storage/MemoryStorageAdapter.ts +12 -0
- package/ts/tsmdb/storage/WAL.ts +375 -0
- package/ts/tsmdb/utils/checksum.ts +88 -0
- package/ts/tsmdb/utils/index.ts +1 -0
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import * as plugins from '../tsmdb.plugins.js';
|
|
2
2
|
import type { IStorageAdapter } from './IStorageAdapter.js';
|
|
3
3
|
import type { IStoredDocument, IOpLogEntry, Document } from '../types/interfaces.js';
|
|
4
|
+
import { calculateDocumentChecksum, verifyChecksum } from '../utils/checksum.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* File storage adapter options
|
|
8
|
+
*/
|
|
9
|
+
export interface IFileStorageAdapterOptions {
|
|
10
|
+
/** Enable checksum verification for data integrity */
|
|
11
|
+
enableChecksums?: boolean;
|
|
12
|
+
/** Throw error on checksum mismatch (default: false, just log warning) */
|
|
13
|
+
strictChecksums?: boolean;
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
/**
|
|
6
17
|
* File-based storage adapter for TsmDB
|
|
@@ -11,9 +22,13 @@ export class FileStorageAdapter implements IStorageAdapter {
|
|
|
11
22
|
private opLogCounter = 0;
|
|
12
23
|
private initialized = false;
|
|
13
24
|
private fs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
|
25
|
+
private enableChecksums: boolean;
|
|
26
|
+
private strictChecksums: boolean;
|
|
14
27
|
|
|
15
|
-
constructor(basePath: string) {
|
|
28
|
+
constructor(basePath: string, options?: IFileStorageAdapterOptions) {
|
|
16
29
|
this.basePath = basePath;
|
|
30
|
+
this.enableChecksums = options?.enableChecksums ?? false;
|
|
31
|
+
this.strictChecksums = options?.strictChecksums ?? false;
|
|
17
32
|
}
|
|
18
33
|
|
|
19
34
|
// ============================================================================
|
|
@@ -68,6 +83,45 @@ export class FileStorageAdapter implements IStorageAdapter {
|
|
|
68
83
|
return doc;
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Verify document checksum and handle errors
|
|
88
|
+
*/
|
|
89
|
+
private verifyDocumentChecksum(doc: any): boolean {
|
|
90
|
+
if (!this.enableChecksums || !doc._checksum) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const isValid = verifyChecksum(doc);
|
|
95
|
+
if (!isValid) {
|
|
96
|
+
const errorMsg = `Checksum mismatch for document ${doc._id}`;
|
|
97
|
+
if (this.strictChecksums) {
|
|
98
|
+
throw new Error(errorMsg);
|
|
99
|
+
} else {
|
|
100
|
+
console.warn(`WARNING: ${errorMsg}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return isValid;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Add checksum to document before storing
|
|
108
|
+
*/
|
|
109
|
+
private prepareDocumentForStorage(doc: any): any {
|
|
110
|
+
if (!this.enableChecksums) {
|
|
111
|
+
return doc;
|
|
112
|
+
}
|
|
113
|
+
const checksum = calculateDocumentChecksum(doc);
|
|
114
|
+
return { ...doc, _checksum: checksum };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Remove internal checksum field before returning to user
|
|
119
|
+
*/
|
|
120
|
+
private cleanDocumentForReturn(doc: any): IStoredDocument {
|
|
121
|
+
const { _checksum, ...cleanDoc } = doc;
|
|
122
|
+
return this.restoreObjectIds(cleanDoc);
|
|
123
|
+
}
|
|
124
|
+
|
|
71
125
|
// ============================================================================
|
|
72
126
|
// Initialization
|
|
73
127
|
// ============================================================================
|
|
@@ -233,7 +287,9 @@ export class FileStorageAdapter implements IStorageAdapter {
|
|
|
233
287
|
throw new Error(`Duplicate key error: _id ${idStr}`);
|
|
234
288
|
}
|
|
235
289
|
|
|
236
|
-
|
|
290
|
+
// Add checksum if enabled
|
|
291
|
+
const docToStore = this.prepareDocumentForStorage(storedDoc);
|
|
292
|
+
docs.push(docToStore);
|
|
237
293
|
await this.writeJsonFile(collPath, docs);
|
|
238
294
|
return storedDoc;
|
|
239
295
|
}
|
|
@@ -258,7 +314,9 @@ export class FileStorageAdapter implements IStorageAdapter {
|
|
|
258
314
|
}
|
|
259
315
|
|
|
260
316
|
existingIds.add(idStr);
|
|
261
|
-
|
|
317
|
+
// Add checksum if enabled
|
|
318
|
+
const docToStore = this.prepareDocumentForStorage(storedDoc);
|
|
319
|
+
docs.push(docToStore);
|
|
262
320
|
results.push(storedDoc);
|
|
263
321
|
}
|
|
264
322
|
|
|
@@ -270,10 +328,33 @@ export class FileStorageAdapter implements IStorageAdapter {
|
|
|
270
328
|
await this.createCollection(dbName, collName);
|
|
271
329
|
const collPath = this.getCollectionPath(dbName, collName);
|
|
272
330
|
const docs = await this.readJsonFile<any[]>(collPath, []);
|
|
273
|
-
return docs.map(doc =>
|
|
331
|
+
return docs.map(doc => {
|
|
332
|
+
// Verify checksum if enabled
|
|
333
|
+
this.verifyDocumentChecksum(doc);
|
|
334
|
+
// Clean and return document without internal checksum field
|
|
335
|
+
return this.cleanDocumentForReturn(doc);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async findByIds(dbName: string, collName: string, ids: Set<string>): Promise<IStoredDocument[]> {
|
|
340
|
+
await this.createCollection(dbName, collName);
|
|
341
|
+
const collPath = this.getCollectionPath(dbName, collName);
|
|
342
|
+
const docs = await this.readJsonFile<any[]>(collPath, []);
|
|
343
|
+
const results: IStoredDocument[] = [];
|
|
344
|
+
for (const doc of docs) {
|
|
345
|
+
// Verify checksum if enabled
|
|
346
|
+
this.verifyDocumentChecksum(doc);
|
|
347
|
+
// Clean and restore document
|
|
348
|
+
const cleaned = this.cleanDocumentForReturn(doc);
|
|
349
|
+
if (ids.has(cleaned._id.toHexString())) {
|
|
350
|
+
results.push(cleaned);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return results;
|
|
274
354
|
}
|
|
275
355
|
|
|
276
356
|
async findById(dbName: string, collName: string, id: plugins.bson.ObjectId): Promise<IStoredDocument | null> {
|
|
357
|
+
// Use findAll which already handles checksum verification
|
|
277
358
|
const docs = await this.findAll(dbName, collName);
|
|
278
359
|
const idStr = id.toHexString();
|
|
279
360
|
return docs.find(d => d._id.toHexString() === idStr) || null;
|
|
@@ -291,7 +372,9 @@ export class FileStorageAdapter implements IStorageAdapter {
|
|
|
291
372
|
|
|
292
373
|
if (idx === -1) return false;
|
|
293
374
|
|
|
294
|
-
|
|
375
|
+
// Add checksum if enabled
|
|
376
|
+
const docToStore = this.prepareDocumentForStorage(doc);
|
|
377
|
+
docs[idx] = docToStore;
|
|
295
378
|
await this.writeJsonFile(collPath, docs);
|
|
296
379
|
return true;
|
|
297
380
|
}
|
|
@@ -90,6 +90,12 @@ export interface IStorageAdapter {
|
|
|
90
90
|
*/
|
|
91
91
|
findAll(dbName: string, collName: string): Promise<IStoredDocument[]>;
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Find documents by a set of _id strings (hex format)
|
|
95
|
+
* Used for index-accelerated queries
|
|
96
|
+
*/
|
|
97
|
+
findByIds(dbName: string, collName: string, ids: Set<string>): Promise<IStoredDocument[]>;
|
|
98
|
+
|
|
93
99
|
/**
|
|
94
100
|
* Find a document by _id
|
|
95
101
|
*/
|
|
@@ -196,6 +196,18 @@ export class MemoryStorageAdapter implements IStorageAdapter {
|
|
|
196
196
|
return Array.from(collection.values());
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
async findByIds(dbName: string, collName: string, ids: Set<string>): Promise<IStoredDocument[]> {
|
|
200
|
+
const collection = this.ensureCollection(dbName, collName);
|
|
201
|
+
const results: IStoredDocument[] = [];
|
|
202
|
+
for (const id of ids) {
|
|
203
|
+
const doc = collection.get(id);
|
|
204
|
+
if (doc) {
|
|
205
|
+
results.push(doc);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
|
|
199
211
|
async findById(dbName: string, collName: string, id: plugins.bson.ObjectId): Promise<IStoredDocument | null> {
|
|
200
212
|
const collection = this.ensureCollection(dbName, collName);
|
|
201
213
|
return collection.get(id.toHexString()) || null;
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import * as plugins from '../tsmdb.plugins.js';
|
|
2
|
+
import type { Document, IStoredDocument } from '../types/interfaces.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WAL entry operation types
|
|
6
|
+
*/
|
|
7
|
+
export type TWalOperation = 'insert' | 'update' | 'delete' | 'checkpoint' | 'begin' | 'commit' | 'abort';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* WAL entry structure
|
|
11
|
+
*/
|
|
12
|
+
export interface IWalEntry {
|
|
13
|
+
/** Log Sequence Number - monotonically increasing */
|
|
14
|
+
lsn: number;
|
|
15
|
+
/** Timestamp of the operation */
|
|
16
|
+
timestamp: number;
|
|
17
|
+
/** Operation type */
|
|
18
|
+
operation: TWalOperation;
|
|
19
|
+
/** Database name */
|
|
20
|
+
dbName: string;
|
|
21
|
+
/** Collection name */
|
|
22
|
+
collName: string;
|
|
23
|
+
/** Document ID (hex string) */
|
|
24
|
+
documentId: string;
|
|
25
|
+
/** Document data (BSON serialized, base64 encoded) */
|
|
26
|
+
data?: string;
|
|
27
|
+
/** Previous document data for updates (for rollback) */
|
|
28
|
+
previousData?: string;
|
|
29
|
+
/** Transaction ID if part of a transaction */
|
|
30
|
+
txnId?: string;
|
|
31
|
+
/** CRC32 checksum of the entry (excluding this field) */
|
|
32
|
+
checksum: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checkpoint record
|
|
37
|
+
*/
|
|
38
|
+
interface ICheckpointRecord {
|
|
39
|
+
lsn: number;
|
|
40
|
+
timestamp: number;
|
|
41
|
+
lastCommittedLsn: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Write-Ahead Log (WAL) for durability and crash recovery
|
|
46
|
+
*
|
|
47
|
+
* The WAL ensures durability by writing operations to a log file before
|
|
48
|
+
* they are applied to the main storage. On crash recovery, uncommitted
|
|
49
|
+
* operations can be replayed to restore the database to a consistent state.
|
|
50
|
+
*/
|
|
51
|
+
export class WAL {
|
|
52
|
+
private walPath: string;
|
|
53
|
+
private currentLsn: number = 0;
|
|
54
|
+
private lastCheckpointLsn: number = 0;
|
|
55
|
+
private entries: IWalEntry[] = [];
|
|
56
|
+
private isInitialized: boolean = false;
|
|
57
|
+
private fs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
|
58
|
+
|
|
59
|
+
// In-memory uncommitted entries per transaction
|
|
60
|
+
private uncommittedTxns: Map<string, IWalEntry[]> = new Map();
|
|
61
|
+
|
|
62
|
+
// Checkpoint interval (number of entries between checkpoints)
|
|
63
|
+
private checkpointInterval: number = 1000;
|
|
64
|
+
|
|
65
|
+
constructor(walPath: string, options?: { checkpointInterval?: number }) {
|
|
66
|
+
this.walPath = walPath;
|
|
67
|
+
if (options?.checkpointInterval) {
|
|
68
|
+
this.checkpointInterval = options.checkpointInterval;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initialize the WAL, loading existing entries and recovering if needed
|
|
74
|
+
*/
|
|
75
|
+
async initialize(): Promise<{ recoveredEntries: IWalEntry[] }> {
|
|
76
|
+
if (this.isInitialized) {
|
|
77
|
+
return { recoveredEntries: [] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Ensure WAL directory exists
|
|
81
|
+
const walDir = this.walPath.substring(0, this.walPath.lastIndexOf('/'));
|
|
82
|
+
if (walDir) {
|
|
83
|
+
await this.fs.directory(walDir).recursive().create();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Try to load existing WAL
|
|
87
|
+
const exists = await this.fs.file(this.walPath).exists();
|
|
88
|
+
if (exists) {
|
|
89
|
+
const content = await this.fs.file(this.walPath).encoding('utf8').read();
|
|
90
|
+
const lines = (content as string).split('\n').filter(line => line.trim());
|
|
91
|
+
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
try {
|
|
94
|
+
const entry = JSON.parse(line) as IWalEntry;
|
|
95
|
+
// Verify checksum
|
|
96
|
+
if (this.verifyChecksum(entry)) {
|
|
97
|
+
this.entries.push(entry);
|
|
98
|
+
if (entry.lsn > this.currentLsn) {
|
|
99
|
+
this.currentLsn = entry.lsn;
|
|
100
|
+
}
|
|
101
|
+
if (entry.operation === 'checkpoint') {
|
|
102
|
+
this.lastCheckpointLsn = entry.lsn;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Skip corrupted entries
|
|
107
|
+
console.warn('Skipping corrupted WAL entry');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.isInitialized = true;
|
|
113
|
+
|
|
114
|
+
// Return entries after last checkpoint that need recovery
|
|
115
|
+
const recoveredEntries = this.entries.filter(
|
|
116
|
+
e => e.lsn > this.lastCheckpointLsn &&
|
|
117
|
+
(e.operation === 'insert' || e.operation === 'update' || e.operation === 'delete')
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return { recoveredEntries };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Log an insert operation
|
|
125
|
+
*/
|
|
126
|
+
async logInsert(dbName: string, collName: string, doc: IStoredDocument, txnId?: string): Promise<number> {
|
|
127
|
+
return this.appendEntry({
|
|
128
|
+
operation: 'insert',
|
|
129
|
+
dbName,
|
|
130
|
+
collName,
|
|
131
|
+
documentId: doc._id.toHexString(),
|
|
132
|
+
data: this.serializeDocument(doc),
|
|
133
|
+
txnId,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Log an update operation
|
|
139
|
+
*/
|
|
140
|
+
async logUpdate(
|
|
141
|
+
dbName: string,
|
|
142
|
+
collName: string,
|
|
143
|
+
oldDoc: IStoredDocument,
|
|
144
|
+
newDoc: IStoredDocument,
|
|
145
|
+
txnId?: string
|
|
146
|
+
): Promise<number> {
|
|
147
|
+
return this.appendEntry({
|
|
148
|
+
operation: 'update',
|
|
149
|
+
dbName,
|
|
150
|
+
collName,
|
|
151
|
+
documentId: oldDoc._id.toHexString(),
|
|
152
|
+
data: this.serializeDocument(newDoc),
|
|
153
|
+
previousData: this.serializeDocument(oldDoc),
|
|
154
|
+
txnId,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Log a delete operation
|
|
160
|
+
*/
|
|
161
|
+
async logDelete(dbName: string, collName: string, doc: IStoredDocument, txnId?: string): Promise<number> {
|
|
162
|
+
return this.appendEntry({
|
|
163
|
+
operation: 'delete',
|
|
164
|
+
dbName,
|
|
165
|
+
collName,
|
|
166
|
+
documentId: doc._id.toHexString(),
|
|
167
|
+
previousData: this.serializeDocument(doc),
|
|
168
|
+
txnId,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Log transaction begin
|
|
174
|
+
*/
|
|
175
|
+
async logBeginTransaction(txnId: string): Promise<number> {
|
|
176
|
+
this.uncommittedTxns.set(txnId, []);
|
|
177
|
+
return this.appendEntry({
|
|
178
|
+
operation: 'begin',
|
|
179
|
+
dbName: '',
|
|
180
|
+
collName: '',
|
|
181
|
+
documentId: '',
|
|
182
|
+
txnId,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Log transaction commit
|
|
188
|
+
*/
|
|
189
|
+
async logCommitTransaction(txnId: string): Promise<number> {
|
|
190
|
+
this.uncommittedTxns.delete(txnId);
|
|
191
|
+
return this.appendEntry({
|
|
192
|
+
operation: 'commit',
|
|
193
|
+
dbName: '',
|
|
194
|
+
collName: '',
|
|
195
|
+
documentId: '',
|
|
196
|
+
txnId,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Log transaction abort
|
|
202
|
+
*/
|
|
203
|
+
async logAbortTransaction(txnId: string): Promise<number> {
|
|
204
|
+
this.uncommittedTxns.delete(txnId);
|
|
205
|
+
return this.appendEntry({
|
|
206
|
+
operation: 'abort',
|
|
207
|
+
dbName: '',
|
|
208
|
+
collName: '',
|
|
209
|
+
documentId: '',
|
|
210
|
+
txnId,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get entries to roll back for an aborted transaction
|
|
216
|
+
*/
|
|
217
|
+
getTransactionEntries(txnId: string): IWalEntry[] {
|
|
218
|
+
return this.entries.filter(e => e.txnId === txnId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a checkpoint - marks a consistent point in the log
|
|
223
|
+
*/
|
|
224
|
+
async checkpoint(): Promise<number> {
|
|
225
|
+
const lsn = await this.appendEntry({
|
|
226
|
+
operation: 'checkpoint',
|
|
227
|
+
dbName: '',
|
|
228
|
+
collName: '',
|
|
229
|
+
documentId: '',
|
|
230
|
+
});
|
|
231
|
+
this.lastCheckpointLsn = lsn;
|
|
232
|
+
|
|
233
|
+
// Truncate old entries (keep only entries after checkpoint)
|
|
234
|
+
await this.truncate();
|
|
235
|
+
|
|
236
|
+
return lsn;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Truncate the WAL file, removing entries before the last checkpoint
|
|
241
|
+
*/
|
|
242
|
+
private async truncate(): Promise<void> {
|
|
243
|
+
// Keep entries after last checkpoint
|
|
244
|
+
const newEntries = this.entries.filter(e => e.lsn >= this.lastCheckpointLsn);
|
|
245
|
+
this.entries = newEntries;
|
|
246
|
+
|
|
247
|
+
// Rewrite the WAL file
|
|
248
|
+
const lines = this.entries.map(e => JSON.stringify(e)).join('\n');
|
|
249
|
+
await this.fs.file(this.walPath).encoding('utf8').write(lines);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get current LSN
|
|
254
|
+
*/
|
|
255
|
+
getCurrentLsn(): number {
|
|
256
|
+
return this.currentLsn;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get entries after a specific LSN (for recovery)
|
|
261
|
+
*/
|
|
262
|
+
getEntriesAfter(lsn: number): IWalEntry[] {
|
|
263
|
+
return this.entries.filter(e => e.lsn > lsn);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Close the WAL
|
|
268
|
+
*/
|
|
269
|
+
async close(): Promise<void> {
|
|
270
|
+
if (this.isInitialized) {
|
|
271
|
+
// Final checkpoint before close
|
|
272
|
+
await this.checkpoint();
|
|
273
|
+
}
|
|
274
|
+
this.isInitialized = false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// Private Methods
|
|
279
|
+
// ============================================================================
|
|
280
|
+
|
|
281
|
+
private async appendEntry(
|
|
282
|
+
partial: Omit<IWalEntry, 'lsn' | 'timestamp' | 'checksum'>
|
|
283
|
+
): Promise<number> {
|
|
284
|
+
await this.initialize();
|
|
285
|
+
|
|
286
|
+
this.currentLsn++;
|
|
287
|
+
const entry: IWalEntry = {
|
|
288
|
+
...partial,
|
|
289
|
+
lsn: this.currentLsn,
|
|
290
|
+
timestamp: Date.now(),
|
|
291
|
+
checksum: 0, // Will be calculated
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Calculate checksum
|
|
295
|
+
entry.checksum = this.calculateChecksum(entry);
|
|
296
|
+
|
|
297
|
+
// Track in transaction if applicable
|
|
298
|
+
if (partial.txnId && this.uncommittedTxns.has(partial.txnId)) {
|
|
299
|
+
this.uncommittedTxns.get(partial.txnId)!.push(entry);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Add to in-memory log
|
|
303
|
+
this.entries.push(entry);
|
|
304
|
+
|
|
305
|
+
// Append to file (append mode for durability)
|
|
306
|
+
await this.fs.file(this.walPath).encoding('utf8').append(JSON.stringify(entry) + '\n');
|
|
307
|
+
|
|
308
|
+
// Check if we need a checkpoint
|
|
309
|
+
if (this.entries.length - this.lastCheckpointLsn >= this.checkpointInterval) {
|
|
310
|
+
await this.checkpoint();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return entry.lsn;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private serializeDocument(doc: Document): string {
|
|
317
|
+
// Serialize document to BSON and encode as base64
|
|
318
|
+
const bsonData = plugins.bson.serialize(doc);
|
|
319
|
+
return Buffer.from(bsonData).toString('base64');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private deserializeDocument(data: string): Document {
|
|
323
|
+
// Decode base64 and deserialize from BSON
|
|
324
|
+
const buffer = Buffer.from(data, 'base64');
|
|
325
|
+
return plugins.bson.deserialize(buffer);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private calculateChecksum(entry: IWalEntry): number {
|
|
329
|
+
// Simple CRC32-like checksum
|
|
330
|
+
const str = JSON.stringify({
|
|
331
|
+
lsn: entry.lsn,
|
|
332
|
+
timestamp: entry.timestamp,
|
|
333
|
+
operation: entry.operation,
|
|
334
|
+
dbName: entry.dbName,
|
|
335
|
+
collName: entry.collName,
|
|
336
|
+
documentId: entry.documentId,
|
|
337
|
+
data: entry.data,
|
|
338
|
+
previousData: entry.previousData,
|
|
339
|
+
txnId: entry.txnId,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
let crc = 0xFFFFFFFF;
|
|
343
|
+
for (let i = 0; i < str.length; i++) {
|
|
344
|
+
crc ^= str.charCodeAt(i);
|
|
345
|
+
for (let j = 0; j < 8; j++) {
|
|
346
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xEDB88320 : 0);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return (~crc) >>> 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private verifyChecksum(entry: IWalEntry): boolean {
|
|
353
|
+
const savedChecksum = entry.checksum;
|
|
354
|
+
entry.checksum = 0;
|
|
355
|
+
const calculatedChecksum = this.calculateChecksum(entry);
|
|
356
|
+
entry.checksum = savedChecksum;
|
|
357
|
+
return calculatedChecksum === savedChecksum;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Recover document from WAL entry
|
|
362
|
+
*/
|
|
363
|
+
recoverDocument(entry: IWalEntry): IStoredDocument | null {
|
|
364
|
+
if (!entry.data) return null;
|
|
365
|
+
return this.deserializeDocument(entry.data) as IStoredDocument;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Recover previous document state from WAL entry (for rollback)
|
|
370
|
+
*/
|
|
371
|
+
recoverPreviousDocument(entry: IWalEntry): IStoredDocument | null {
|
|
372
|
+
if (!entry.previousData) return null;
|
|
373
|
+
return this.deserializeDocument(entry.previousData) as IStoredDocument;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRC32 checksum utilities for data integrity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// CRC32 lookup table
|
|
6
|
+
const CRC32_TABLE: number[] = [];
|
|
7
|
+
|
|
8
|
+
// Initialize the CRC32 table
|
|
9
|
+
function initCRC32Table(): void {
|
|
10
|
+
if (CRC32_TABLE.length > 0) return;
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < 256; i++) {
|
|
13
|
+
let crc = i;
|
|
14
|
+
for (let j = 0; j < 8; j++) {
|
|
15
|
+
crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
|
|
16
|
+
}
|
|
17
|
+
CRC32_TABLE[i] = crc >>> 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Calculate CRC32 checksum for a string
|
|
23
|
+
*/
|
|
24
|
+
export function calculateCRC32(data: string): number {
|
|
25
|
+
initCRC32Table();
|
|
26
|
+
|
|
27
|
+
let crc = 0xFFFFFFFF;
|
|
28
|
+
for (let i = 0; i < data.length; i++) {
|
|
29
|
+
const byte = data.charCodeAt(i) & 0xFF;
|
|
30
|
+
crc = CRC32_TABLE[(crc ^ byte) & 0xFF] ^ (crc >>> 8);
|
|
31
|
+
}
|
|
32
|
+
return (~crc) >>> 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Calculate CRC32 checksum for a Buffer
|
|
37
|
+
*/
|
|
38
|
+
export function calculateCRC32Buffer(data: Buffer): number {
|
|
39
|
+
initCRC32Table();
|
|
40
|
+
|
|
41
|
+
let crc = 0xFFFFFFFF;
|
|
42
|
+
for (let i = 0; i < data.length; i++) {
|
|
43
|
+
crc = CRC32_TABLE[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
|
|
44
|
+
}
|
|
45
|
+
return (~crc) >>> 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate checksum for a document (serialized as JSON)
|
|
50
|
+
*/
|
|
51
|
+
export function calculateDocumentChecksum(doc: Record<string, any>): number {
|
|
52
|
+
// Exclude _checksum field from calculation
|
|
53
|
+
const { _checksum, ...docWithoutChecksum } = doc;
|
|
54
|
+
const json = JSON.stringify(docWithoutChecksum);
|
|
55
|
+
return calculateCRC32(json);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Add checksum to a document
|
|
60
|
+
*/
|
|
61
|
+
export function addChecksum<T extends Record<string, any>>(doc: T): T & { _checksum: number } {
|
|
62
|
+
const checksum = calculateDocumentChecksum(doc);
|
|
63
|
+
return { ...doc, _checksum: checksum };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Verify checksum of a document
|
|
68
|
+
* Returns true if checksum is valid or if document has no checksum
|
|
69
|
+
*/
|
|
70
|
+
export function verifyChecksum(doc: Record<string, any>): boolean {
|
|
71
|
+
if (!('_checksum' in doc)) {
|
|
72
|
+
// No checksum to verify
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const storedChecksum = doc._checksum;
|
|
77
|
+
const calculatedChecksum = calculateDocumentChecksum(doc);
|
|
78
|
+
|
|
79
|
+
return storedChecksum === calculatedChecksum;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Remove checksum from a document
|
|
84
|
+
*/
|
|
85
|
+
export function removeChecksum<T extends Record<string, any>>(doc: T): Omit<T, '_checksum'> {
|
|
86
|
+
const { _checksum, ...docWithoutChecksum } = doc;
|
|
87
|
+
return docWithoutChecksum as Omit<T, '_checksum'>;
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './checksum.js';
|