@push.rocks/smartmongo 3.0.0 → 4.0.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 (47) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/tsmdb/engine/IndexEngine.d.ts +23 -3
  3. package/dist_ts/tsmdb/engine/IndexEngine.js +357 -55
  4. package/dist_ts/tsmdb/engine/QueryPlanner.d.ts +64 -0
  5. package/dist_ts/tsmdb/engine/QueryPlanner.js +308 -0
  6. package/dist_ts/tsmdb/engine/SessionEngine.d.ts +117 -0
  7. package/dist_ts/tsmdb/engine/SessionEngine.js +232 -0
  8. package/dist_ts/tsmdb/index.d.ts +7 -0
  9. package/dist_ts/tsmdb/index.js +6 -1
  10. package/dist_ts/tsmdb/server/CommandRouter.d.ts +36 -0
  11. package/dist_ts/tsmdb/server/CommandRouter.js +91 -1
  12. package/dist_ts/tsmdb/server/TsmdbServer.js +3 -1
  13. package/dist_ts/tsmdb/server/handlers/AdminHandler.js +106 -6
  14. package/dist_ts/tsmdb/server/handlers/DeleteHandler.js +15 -3
  15. package/dist_ts/tsmdb/server/handlers/FindHandler.js +44 -14
  16. package/dist_ts/tsmdb/server/handlers/InsertHandler.js +4 -1
  17. package/dist_ts/tsmdb/server/handlers/UpdateHandler.js +31 -5
  18. package/dist_ts/tsmdb/storage/FileStorageAdapter.d.ts +25 -1
  19. package/dist_ts/tsmdb/storage/FileStorageAdapter.js +75 -6
  20. package/dist_ts/tsmdb/storage/IStorageAdapter.d.ts +5 -0
  21. package/dist_ts/tsmdb/storage/MemoryStorageAdapter.d.ts +1 -0
  22. package/dist_ts/tsmdb/storage/MemoryStorageAdapter.js +12 -1
  23. package/dist_ts/tsmdb/storage/WAL.d.ts +117 -0
  24. package/dist_ts/tsmdb/storage/WAL.js +286 -0
  25. package/dist_ts/tsmdb/utils/checksum.d.ts +30 -0
  26. package/dist_ts/tsmdb/utils/checksum.js +77 -0
  27. package/dist_ts/tsmdb/utils/index.d.ts +1 -0
  28. package/dist_ts/tsmdb/utils/index.js +2 -0
  29. package/package.json +1 -1
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/tsmdb/engine/IndexEngine.ts +375 -56
  32. package/ts/tsmdb/engine/QueryPlanner.ts +393 -0
  33. package/ts/tsmdb/engine/SessionEngine.ts +292 -0
  34. package/ts/tsmdb/index.ts +9 -0
  35. package/ts/tsmdb/server/CommandRouter.ts +109 -0
  36. package/ts/tsmdb/server/TsmdbServer.ts +3 -0
  37. package/ts/tsmdb/server/handlers/AdminHandler.ts +110 -5
  38. package/ts/tsmdb/server/handlers/DeleteHandler.ts +17 -2
  39. package/ts/tsmdb/server/handlers/FindHandler.ts +42 -13
  40. package/ts/tsmdb/server/handlers/InsertHandler.ts +6 -0
  41. package/ts/tsmdb/server/handlers/UpdateHandler.ts +33 -4
  42. package/ts/tsmdb/storage/FileStorageAdapter.ts +88 -5
  43. package/ts/tsmdb/storage/IStorageAdapter.ts +6 -0
  44. package/ts/tsmdb/storage/MemoryStorageAdapter.ts +12 -0
  45. package/ts/tsmdb/storage/WAL.ts +375 -0
  46. package/ts/tsmdb/utils/checksum.ts +88 -0
  47. package/ts/tsmdb/utils/index.ts +1 -0
@@ -0,0 +1,393 @@
1
+ import * as plugins from '../tsmdb.plugins.js';
2
+ import type { Document, IStoredDocument } from '../types/interfaces.js';
3
+ import { IndexEngine } from './IndexEngine.js';
4
+
5
+ /**
6
+ * Query execution plan types
7
+ */
8
+ export type TQueryPlanType = 'IXSCAN' | 'COLLSCAN' | 'FETCH' | 'IXSCAN_RANGE';
9
+
10
+ /**
11
+ * Represents a query execution plan
12
+ */
13
+ export interface IQueryPlan {
14
+ /** The type of scan used */
15
+ type: TQueryPlanType;
16
+ /** Index name if using an index */
17
+ indexName?: string;
18
+ /** Index key specification */
19
+ indexKey?: Record<string, 1 | -1 | string>;
20
+ /** Whether the query can be fully satisfied by the index */
21
+ indexCovering: boolean;
22
+ /** Estimated selectivity (0-1, lower is more selective) */
23
+ selectivity: number;
24
+ /** Whether range operators are used */
25
+ usesRange: boolean;
26
+ /** Fields used from the index */
27
+ indexFieldsUsed: string[];
28
+ /** Filter conditions that must be applied post-index lookup */
29
+ residualFilter?: Document;
30
+ /** Explanation for debugging */
31
+ explanation: string;
32
+ }
33
+
34
+ /**
35
+ * Filter operator analysis
36
+ */
37
+ interface IFilterOperatorInfo {
38
+ field: string;
39
+ operators: string[];
40
+ equality: boolean;
41
+ range: boolean;
42
+ in: boolean;
43
+ exists: boolean;
44
+ regex: boolean;
45
+ values: Record<string, any>;
46
+ }
47
+
48
+ /**
49
+ * QueryPlanner - Analyzes queries and selects optimal execution plans
50
+ */
51
+ export class QueryPlanner {
52
+ private indexEngine: IndexEngine;
53
+
54
+ constructor(indexEngine: IndexEngine) {
55
+ this.indexEngine = indexEngine;
56
+ }
57
+
58
+ /**
59
+ * Generate an execution plan for a query filter
60
+ */
61
+ async plan(filter: Document): Promise<IQueryPlan> {
62
+ await this.indexEngine['initialize']();
63
+
64
+ // Empty filter = full collection scan
65
+ if (!filter || Object.keys(filter).length === 0) {
66
+ return {
67
+ type: 'COLLSCAN',
68
+ indexCovering: false,
69
+ selectivity: 1.0,
70
+ usesRange: false,
71
+ indexFieldsUsed: [],
72
+ explanation: 'No filter specified, full collection scan required',
73
+ };
74
+ }
75
+
76
+ // Analyze the filter
77
+ const operatorInfo = this.analyzeFilter(filter);
78
+
79
+ // Get available indexes
80
+ const indexes = await this.indexEngine.listIndexes();
81
+
82
+ // Score each index
83
+ let bestPlan: IQueryPlan | null = null;
84
+ let bestScore = -1;
85
+
86
+ for (const index of indexes) {
87
+ const plan = this.scoreIndex(index, operatorInfo, filter);
88
+ if (plan.selectivity < 1.0) {
89
+ const score = this.calculateScore(plan);
90
+ if (score > bestScore) {
91
+ bestScore = score;
92
+ bestPlan = plan;
93
+ }
94
+ }
95
+ }
96
+
97
+ // If no suitable index found, fall back to collection scan
98
+ if (!bestPlan || bestScore <= 0) {
99
+ return {
100
+ type: 'COLLSCAN',
101
+ indexCovering: false,
102
+ selectivity: 1.0,
103
+ usesRange: false,
104
+ indexFieldsUsed: [],
105
+ explanation: 'No suitable index found for this query',
106
+ };
107
+ }
108
+
109
+ return bestPlan;
110
+ }
111
+
112
+ /**
113
+ * Analyze filter to extract operator information per field
114
+ */
115
+ private analyzeFilter(filter: Document, prefix = ''): Map<string, IFilterOperatorInfo> {
116
+ const result = new Map<string, IFilterOperatorInfo>();
117
+
118
+ for (const [key, value] of Object.entries(filter)) {
119
+ // Skip logical operators at the top level
120
+ if (key.startsWith('$')) {
121
+ if (key === '$and' && Array.isArray(value)) {
122
+ // Merge $and conditions
123
+ for (const subFilter of value) {
124
+ const subInfo = this.analyzeFilter(subFilter, prefix);
125
+ for (const [field, info] of subInfo) {
126
+ if (result.has(field)) {
127
+ // Merge operators
128
+ const existing = result.get(field)!;
129
+ existing.operators.push(...info.operators);
130
+ existing.equality = existing.equality || info.equality;
131
+ existing.range = existing.range || info.range;
132
+ existing.in = existing.in || info.in;
133
+ Object.assign(existing.values, info.values);
134
+ } else {
135
+ result.set(field, info);
136
+ }
137
+ }
138
+ }
139
+ }
140
+ continue;
141
+ }
142
+
143
+ const fullKey = prefix ? `${prefix}.${key}` : key;
144
+ const info: IFilterOperatorInfo = {
145
+ field: fullKey,
146
+ operators: [],
147
+ equality: false,
148
+ range: false,
149
+ in: false,
150
+ exists: false,
151
+ regex: false,
152
+ values: {},
153
+ };
154
+
155
+ if (typeof value !== 'object' || value === null || value instanceof plugins.bson.ObjectId || value instanceof Date) {
156
+ // Direct equality
157
+ info.equality = true;
158
+ info.operators.push('$eq');
159
+ info.values['$eq'] = value;
160
+ } else if (Array.isArray(value)) {
161
+ // Array equality (rare, but possible)
162
+ info.equality = true;
163
+ info.operators.push('$eq');
164
+ info.values['$eq'] = value;
165
+ } else {
166
+ // Operator object
167
+ for (const [op, opValue] of Object.entries(value)) {
168
+ if (op.startsWith('$')) {
169
+ info.operators.push(op);
170
+ info.values[op] = opValue;
171
+
172
+ switch (op) {
173
+ case '$eq':
174
+ info.equality = true;
175
+ break;
176
+ case '$ne':
177
+ case '$not':
178
+ // These can use indexes but with low selectivity
179
+ break;
180
+ case '$in':
181
+ info.in = true;
182
+ break;
183
+ case '$nin':
184
+ // Can't efficiently use indexes
185
+ break;
186
+ case '$gt':
187
+ case '$gte':
188
+ case '$lt':
189
+ case '$lte':
190
+ info.range = true;
191
+ break;
192
+ case '$exists':
193
+ info.exists = true;
194
+ break;
195
+ case '$regex':
196
+ info.regex = true;
197
+ break;
198
+ }
199
+ } else {
200
+ // Nested object - recurse
201
+ const nestedInfo = this.analyzeFilter({ [op]: opValue }, fullKey);
202
+ for (const [nestedField, nestedFieldInfo] of nestedInfo) {
203
+ result.set(nestedField, nestedFieldInfo);
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ if (info.operators.length > 0) {
210
+ result.set(fullKey, info);
211
+ }
212
+ }
213
+
214
+ return result;
215
+ }
216
+
217
+ /**
218
+ * Score an index for the given filter
219
+ */
220
+ private scoreIndex(
221
+ index: { name: string; key: Record<string, any>; unique?: boolean; sparse?: boolean },
222
+ operatorInfo: Map<string, IFilterOperatorInfo>,
223
+ filter: Document
224
+ ): IQueryPlan {
225
+ const indexFields = Object.keys(index.key);
226
+ const usedFields: string[] = [];
227
+ let usesRange = false;
228
+ let canUseIndex = true;
229
+ let selectivity = 1.0;
230
+ let residualFilter: Document | undefined;
231
+
232
+ // Check each index field in order
233
+ for (const field of indexFields) {
234
+ const info = operatorInfo.get(field);
235
+ if (!info) {
236
+ // Index field not in filter - stop here
237
+ break;
238
+ }
239
+
240
+ usedFields.push(field);
241
+
242
+ // Calculate selectivity based on operator
243
+ if (info.equality) {
244
+ // Equality has high selectivity
245
+ selectivity *= 0.01; // Assume 1% match
246
+ } else if (info.in) {
247
+ // $in selectivity depends on array size
248
+ const inValues = info.values['$in'];
249
+ if (Array.isArray(inValues)) {
250
+ selectivity *= Math.min(0.5, inValues.length * 0.01);
251
+ } else {
252
+ selectivity *= 0.1;
253
+ }
254
+ } else if (info.range) {
255
+ // Range queries have moderate selectivity
256
+ selectivity *= 0.25;
257
+ usesRange = true;
258
+ // After range, can't use more index fields efficiently
259
+ break;
260
+ } else if (info.exists) {
261
+ // $exists can use sparse indexes
262
+ selectivity *= 0.5;
263
+ } else {
264
+ // Other operators may not be indexable
265
+ canUseIndex = false;
266
+ break;
267
+ }
268
+ }
269
+
270
+ if (!canUseIndex || usedFields.length === 0) {
271
+ return {
272
+ type: 'COLLSCAN',
273
+ indexCovering: false,
274
+ selectivity: 1.0,
275
+ usesRange: false,
276
+ indexFieldsUsed: [],
277
+ explanation: `Index ${index.name} cannot be used for this query`,
278
+ };
279
+ }
280
+
281
+ // Build residual filter for conditions not covered by index
282
+ const coveredFields = new Set(usedFields);
283
+ const residualConditions: Record<string, any> = {};
284
+ for (const [field, info] of operatorInfo) {
285
+ if (!coveredFields.has(field)) {
286
+ // This field isn't covered by the index
287
+ if (info.equality) {
288
+ residualConditions[field] = info.values['$eq'];
289
+ } else {
290
+ residualConditions[field] = info.values;
291
+ }
292
+ }
293
+ }
294
+
295
+ if (Object.keys(residualConditions).length > 0) {
296
+ residualFilter = residualConditions;
297
+ }
298
+
299
+ // Unique indexes have better selectivity for equality
300
+ if (index.unique && usedFields.length === indexFields.length) {
301
+ selectivity = Math.min(selectivity, 0.001); // At most 1 document
302
+ }
303
+
304
+ return {
305
+ type: usesRange ? 'IXSCAN_RANGE' : 'IXSCAN',
306
+ indexName: index.name,
307
+ indexKey: index.key,
308
+ indexCovering: Object.keys(residualConditions).length === 0,
309
+ selectivity,
310
+ usesRange,
311
+ indexFieldsUsed: usedFields,
312
+ residualFilter,
313
+ explanation: `Using index ${index.name} on fields [${usedFields.join(', ')}]`,
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Calculate overall score for a plan (higher is better)
319
+ */
320
+ private calculateScore(plan: IQueryPlan): number {
321
+ let score = 0;
322
+
323
+ // Lower selectivity is better (fewer documents to fetch)
324
+ score += (1 - plan.selectivity) * 100;
325
+
326
+ // Index covering queries are best
327
+ if (plan.indexCovering) {
328
+ score += 50;
329
+ }
330
+
331
+ // More index fields used is better
332
+ score += plan.indexFieldsUsed.length * 10;
333
+
334
+ // Equality scans are better than range scans
335
+ if (!plan.usesRange) {
336
+ score += 20;
337
+ }
338
+
339
+ return score;
340
+ }
341
+
342
+ /**
343
+ * Explain a query - returns detailed plan information
344
+ */
345
+ async explain(filter: Document): Promise<{
346
+ queryPlanner: {
347
+ plannerVersion: number;
348
+ namespace: string;
349
+ indexFilterSet: boolean;
350
+ winningPlan: IQueryPlan;
351
+ rejectedPlans: IQueryPlan[];
352
+ };
353
+ }> {
354
+ await this.indexEngine['initialize']();
355
+
356
+ // Analyze the filter
357
+ const operatorInfo = this.analyzeFilter(filter);
358
+
359
+ // Get available indexes
360
+ const indexes = await this.indexEngine.listIndexes();
361
+
362
+ // Score all indexes
363
+ const plans: IQueryPlan[] = [];
364
+
365
+ for (const index of indexes) {
366
+ const plan = this.scoreIndex(index, operatorInfo, filter);
367
+ plans.push(plan);
368
+ }
369
+
370
+ // Add collection scan as fallback
371
+ plans.push({
372
+ type: 'COLLSCAN',
373
+ indexCovering: false,
374
+ selectivity: 1.0,
375
+ usesRange: false,
376
+ indexFieldsUsed: [],
377
+ explanation: 'Full collection scan',
378
+ });
379
+
380
+ // Sort by score (best first)
381
+ plans.sort((a, b) => this.calculateScore(b) - this.calculateScore(a));
382
+
383
+ return {
384
+ queryPlanner: {
385
+ plannerVersion: 1,
386
+ namespace: `${this.indexEngine['dbName']}.${this.indexEngine['collName']}`,
387
+ indexFilterSet: false,
388
+ winningPlan: plans[0],
389
+ rejectedPlans: plans.slice(1),
390
+ },
391
+ };
392
+ }
393
+ }
@@ -0,0 +1,292 @@
1
+ import * as plugins from '../tsmdb.plugins.js';
2
+ import type { TransactionEngine } from './TransactionEngine.js';
3
+
4
+ /**
5
+ * Session state
6
+ */
7
+ export interface ISession {
8
+ /** Session ID (UUID) */
9
+ id: string;
10
+ /** Timestamp when the session was created */
11
+ createdAt: number;
12
+ /** Timestamp of the last activity */
13
+ lastActivityAt: number;
14
+ /** Current transaction ID if any */
15
+ txnId?: string;
16
+ /** Transaction number for ordering */
17
+ txnNumber?: number;
18
+ /** Whether the session is in a transaction */
19
+ inTransaction: boolean;
20
+ /** Session metadata */
21
+ metadata?: Record<string, any>;
22
+ }
23
+
24
+ /**
25
+ * Session engine options
26
+ */
27
+ export interface ISessionEngineOptions {
28
+ /** Session timeout in milliseconds (default: 30 minutes) */
29
+ sessionTimeoutMs?: number;
30
+ /** Interval to check for expired sessions in ms (default: 60 seconds) */
31
+ cleanupIntervalMs?: number;
32
+ }
33
+
34
+ /**
35
+ * Session engine for managing client sessions
36
+ * - Tracks session lifecycle (create, touch, end)
37
+ * - Links sessions to transactions
38
+ * - Auto-aborts transactions on session expiry
39
+ */
40
+ export class SessionEngine {
41
+ private sessions: Map<string, ISession> = new Map();
42
+ private sessionTimeoutMs: number;
43
+ private cleanupInterval?: ReturnType<typeof setInterval>;
44
+ private transactionEngine?: TransactionEngine;
45
+
46
+ constructor(options?: ISessionEngineOptions) {
47
+ this.sessionTimeoutMs = options?.sessionTimeoutMs ?? 30 * 60 * 1000; // 30 minutes default
48
+ const cleanupIntervalMs = options?.cleanupIntervalMs ?? 60 * 1000; // 1 minute default
49
+
50
+ // Start cleanup interval
51
+ this.cleanupInterval = setInterval(() => {
52
+ this.cleanupExpiredSessions();
53
+ }, cleanupIntervalMs);
54
+ }
55
+
56
+ /**
57
+ * Set the transaction engine to use for auto-abort
58
+ */
59
+ setTransactionEngine(engine: TransactionEngine): void {
60
+ this.transactionEngine = engine;
61
+ }
62
+
63
+ /**
64
+ * Start a new session
65
+ */
66
+ startSession(sessionId?: string, metadata?: Record<string, any>): ISession {
67
+ const id = sessionId ?? new plugins.bson.UUID().toHexString();
68
+ const now = Date.now();
69
+
70
+ const session: ISession = {
71
+ id,
72
+ createdAt: now,
73
+ lastActivityAt: now,
74
+ inTransaction: false,
75
+ metadata,
76
+ };
77
+
78
+ this.sessions.set(id, session);
79
+ return session;
80
+ }
81
+
82
+ /**
83
+ * Get a session by ID
84
+ */
85
+ getSession(sessionId: string): ISession | undefined {
86
+ const session = this.sessions.get(sessionId);
87
+ if (session && this.isSessionExpired(session)) {
88
+ // Session expired, clean it up
89
+ this.endSession(sessionId);
90
+ return undefined;
91
+ }
92
+ return session;
93
+ }
94
+
95
+ /**
96
+ * Touch a session to update last activity time
97
+ */
98
+ touchSession(sessionId: string): boolean {
99
+ const session = this.sessions.get(sessionId);
100
+ if (!session) return false;
101
+
102
+ if (this.isSessionExpired(session)) {
103
+ this.endSession(sessionId);
104
+ return false;
105
+ }
106
+
107
+ session.lastActivityAt = Date.now();
108
+ return true;
109
+ }
110
+
111
+ /**
112
+ * End a session explicitly
113
+ * This will also abort any active transaction
114
+ */
115
+ async endSession(sessionId: string): Promise<boolean> {
116
+ const session = this.sessions.get(sessionId);
117
+ if (!session) return false;
118
+
119
+ // If session has an active transaction, abort it
120
+ if (session.inTransaction && session.txnId && this.transactionEngine) {
121
+ try {
122
+ await this.transactionEngine.abortTransaction(session.txnId);
123
+ } catch (e) {
124
+ // Ignore abort errors during cleanup
125
+ }
126
+ }
127
+
128
+ this.sessions.delete(sessionId);
129
+ return true;
130
+ }
131
+
132
+ /**
133
+ * Start a transaction in a session
134
+ */
135
+ startTransaction(sessionId: string, txnId: string, txnNumber?: number): boolean {
136
+ const session = this.sessions.get(sessionId);
137
+ if (!session) return false;
138
+
139
+ if (this.isSessionExpired(session)) {
140
+ this.endSession(sessionId);
141
+ return false;
142
+ }
143
+
144
+ session.txnId = txnId;
145
+ session.txnNumber = txnNumber;
146
+ session.inTransaction = true;
147
+ session.lastActivityAt = Date.now();
148
+
149
+ return true;
150
+ }
151
+
152
+ /**
153
+ * End a transaction in a session (commit or abort)
154
+ */
155
+ endTransaction(sessionId: string): boolean {
156
+ const session = this.sessions.get(sessionId);
157
+ if (!session) return false;
158
+
159
+ session.txnId = undefined;
160
+ session.txnNumber = undefined;
161
+ session.inTransaction = false;
162
+ session.lastActivityAt = Date.now();
163
+
164
+ return true;
165
+ }
166
+
167
+ /**
168
+ * Get transaction ID for a session
169
+ */
170
+ getTransactionId(sessionId: string): string | undefined {
171
+ const session = this.sessions.get(sessionId);
172
+ return session?.txnId;
173
+ }
174
+
175
+ /**
176
+ * Check if session is in a transaction
177
+ */
178
+ isInTransaction(sessionId: string): boolean {
179
+ const session = this.sessions.get(sessionId);
180
+ return session?.inTransaction ?? false;
181
+ }
182
+
183
+ /**
184
+ * Check if a session is expired
185
+ */
186
+ isSessionExpired(session: ISession): boolean {
187
+ return Date.now() - session.lastActivityAt > this.sessionTimeoutMs;
188
+ }
189
+
190
+ /**
191
+ * Cleanup expired sessions
192
+ * This is called periodically by the cleanup interval
193
+ */
194
+ private async cleanupExpiredSessions(): Promise<void> {
195
+ const expiredSessions: string[] = [];
196
+
197
+ for (const [id, session] of this.sessions) {
198
+ if (this.isSessionExpired(session)) {
199
+ expiredSessions.push(id);
200
+ }
201
+ }
202
+
203
+ // End all expired sessions (this will also abort their transactions)
204
+ for (const sessionId of expiredSessions) {
205
+ await this.endSession(sessionId);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Get all active sessions
211
+ */
212
+ listSessions(): ISession[] {
213
+ const activeSessions: ISession[] = [];
214
+ for (const session of this.sessions.values()) {
215
+ if (!this.isSessionExpired(session)) {
216
+ activeSessions.push(session);
217
+ }
218
+ }
219
+ return activeSessions;
220
+ }
221
+
222
+ /**
223
+ * Get session count
224
+ */
225
+ getSessionCount(): number {
226
+ return this.sessions.size;
227
+ }
228
+
229
+ /**
230
+ * Get sessions with active transactions
231
+ */
232
+ getSessionsWithTransactions(): ISession[] {
233
+ return this.listSessions().filter(s => s.inTransaction);
234
+ }
235
+
236
+ /**
237
+ * Refresh session timeout
238
+ */
239
+ refreshSession(sessionId: string): boolean {
240
+ return this.touchSession(sessionId);
241
+ }
242
+
243
+ /**
244
+ * Close the session engine and cleanup
245
+ */
246
+ close(): void {
247
+ if (this.cleanupInterval) {
248
+ clearInterval(this.cleanupInterval);
249
+ this.cleanupInterval = undefined;
250
+ }
251
+
252
+ // Clear all sessions
253
+ this.sessions.clear();
254
+ }
255
+
256
+ /**
257
+ * Get or create a session for a given session ID
258
+ * Useful for handling MongoDB driver session requests
259
+ */
260
+ getOrCreateSession(sessionId: string): ISession {
261
+ let session = this.getSession(sessionId);
262
+ if (!session) {
263
+ session = this.startSession(sessionId);
264
+ } else {
265
+ this.touchSession(sessionId);
266
+ }
267
+ return session;
268
+ }
269
+
270
+ /**
271
+ * Extract session ID from MongoDB lsid (logical session ID)
272
+ */
273
+ static extractSessionId(lsid: any): string | undefined {
274
+ if (!lsid) return undefined;
275
+
276
+ // MongoDB session ID format: { id: UUID }
277
+ if (lsid.id) {
278
+ if (lsid.id instanceof plugins.bson.UUID) {
279
+ return lsid.id.toHexString();
280
+ }
281
+ if (typeof lsid.id === 'string') {
282
+ return lsid.id;
283
+ }
284
+ if (lsid.id.$binary?.base64) {
285
+ // Binary UUID format
286
+ return Buffer.from(lsid.id.$binary.base64, 'base64').toString('hex');
287
+ }
288
+ }
289
+
290
+ return undefined;
291
+ }
292
+ }
package/ts/tsmdb/index.ts CHANGED
@@ -19,6 +19,8 @@ export type { IStorageAdapter } from './storage/IStorageAdapter.js';
19
19
  export { MemoryStorageAdapter } from './storage/MemoryStorageAdapter.js';
20
20
  export { FileStorageAdapter } from './storage/FileStorageAdapter.js';
21
21
  export { OpLog } from './storage/OpLog.js';
22
+ export { WAL } from './storage/WAL.js';
23
+ export type { IWalEntry, TWalOperation } from './storage/WAL.js';
22
24
 
23
25
  // Export engines
24
26
  export { QueryEngine } from './engine/QueryEngine.js';
@@ -26,6 +28,10 @@ export { UpdateEngine } from './engine/UpdateEngine.js';
26
28
  export { AggregationEngine } from './engine/AggregationEngine.js';
27
29
  export { IndexEngine } from './engine/IndexEngine.js';
28
30
  export { TransactionEngine } from './engine/TransactionEngine.js';
31
+ export { QueryPlanner } from './engine/QueryPlanner.js';
32
+ export type { IQueryPlan, TQueryPlanType } from './engine/QueryPlanner.js';
33
+ export { SessionEngine } from './engine/SessionEngine.js';
34
+ export type { ISession, ISessionEngineOptions } from './engine/SessionEngine.js';
29
35
 
30
36
  // Export server (the main entry point for using TsmDB)
31
37
  export { TsmdbServer } from './server/TsmdbServer.js';
@@ -35,3 +41,6 @@ export type { ITsmdbServerOptions } from './server/TsmdbServer.js';
35
41
  export { WireProtocol } from './server/WireProtocol.js';
36
42
  export { CommandRouter } from './server/CommandRouter.js';
37
43
  export type { ICommandHandler, IHandlerContext, ICursorState } from './server/CommandRouter.js';
44
+
45
+ // Export utilities
46
+ export * from './utils/checksum.js';