@push.rocks/smartdb 1.0.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.
Files changed (112) hide show
  1. package/.smartconfig.json +38 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +5 -0
  5. package/dist_ts/index.js +8 -0
  6. package/dist_ts/ts_local/classes.localsmartdb.d.ts +78 -0
  7. package/dist_ts/ts_local/classes.localsmartdb.js +115 -0
  8. package/dist_ts/ts_local/index.d.ts +2 -0
  9. package/dist_ts/ts_local/index.js +2 -0
  10. package/dist_ts/ts_local/plugins.d.ts +2 -0
  11. package/dist_ts/ts_local/plugins.js +3 -0
  12. package/dist_ts/ts_smartdb/engine/AggregationEngine.d.ts +66 -0
  13. package/dist_ts/ts_smartdb/engine/AggregationEngine.js +189 -0
  14. package/dist_ts/ts_smartdb/engine/IndexEngine.d.ts +97 -0
  15. package/dist_ts/ts_smartdb/engine/IndexEngine.js +678 -0
  16. package/dist_ts/ts_smartdb/engine/QueryEngine.d.ts +54 -0
  17. package/dist_ts/ts_smartdb/engine/QueryEngine.js +271 -0
  18. package/dist_ts/ts_smartdb/engine/QueryPlanner.d.ts +64 -0
  19. package/dist_ts/ts_smartdb/engine/QueryPlanner.js +308 -0
  20. package/dist_ts/ts_smartdb/engine/SessionEngine.d.ts +117 -0
  21. package/dist_ts/ts_smartdb/engine/SessionEngine.js +232 -0
  22. package/dist_ts/ts_smartdb/engine/TransactionEngine.d.ts +85 -0
  23. package/dist_ts/ts_smartdb/engine/TransactionEngine.js +287 -0
  24. package/dist_ts/ts_smartdb/engine/UpdateEngine.d.ts +47 -0
  25. package/dist_ts/ts_smartdb/engine/UpdateEngine.js +461 -0
  26. package/dist_ts/ts_smartdb/errors/SmartdbErrors.d.ts +100 -0
  27. package/dist_ts/ts_smartdb/errors/SmartdbErrors.js +155 -0
  28. package/dist_ts/ts_smartdb/index.d.ts +26 -0
  29. package/dist_ts/ts_smartdb/index.js +31 -0
  30. package/dist_ts/ts_smartdb/plugins.d.ts +10 -0
  31. package/dist_ts/ts_smartdb/plugins.js +14 -0
  32. package/dist_ts/ts_smartdb/server/CommandRouter.d.ts +87 -0
  33. package/dist_ts/ts_smartdb/server/CommandRouter.js +222 -0
  34. package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +102 -0
  35. package/dist_ts/ts_smartdb/server/SmartdbServer.js +279 -0
  36. package/dist_ts/ts_smartdb/server/WireProtocol.d.ts +117 -0
  37. package/dist_ts/ts_smartdb/server/WireProtocol.js +298 -0
  38. package/dist_ts/ts_smartdb/server/handlers/AdminHandler.d.ts +100 -0
  39. package/dist_ts/ts_smartdb/server/handlers/AdminHandler.js +668 -0
  40. package/dist_ts/ts_smartdb/server/handlers/AggregateHandler.d.ts +31 -0
  41. package/dist_ts/ts_smartdb/server/handlers/AggregateHandler.js +277 -0
  42. package/dist_ts/ts_smartdb/server/handlers/DeleteHandler.d.ts +8 -0
  43. package/dist_ts/ts_smartdb/server/handlers/DeleteHandler.js +95 -0
  44. package/dist_ts/ts_smartdb/server/handlers/FindHandler.d.ts +31 -0
  45. package/dist_ts/ts_smartdb/server/handlers/FindHandler.js +291 -0
  46. package/dist_ts/ts_smartdb/server/handlers/HelloHandler.d.ts +11 -0
  47. package/dist_ts/ts_smartdb/server/handlers/HelloHandler.js +62 -0
  48. package/dist_ts/ts_smartdb/server/handlers/IndexHandler.d.ts +20 -0
  49. package/dist_ts/ts_smartdb/server/handlers/IndexHandler.js +183 -0
  50. package/dist_ts/ts_smartdb/server/handlers/InsertHandler.d.ts +8 -0
  51. package/dist_ts/ts_smartdb/server/handlers/InsertHandler.js +79 -0
  52. package/dist_ts/ts_smartdb/server/handlers/UpdateHandler.d.ts +24 -0
  53. package/dist_ts/ts_smartdb/server/handlers/UpdateHandler.js +296 -0
  54. package/dist_ts/ts_smartdb/server/handlers/index.d.ts +8 -0
  55. package/dist_ts/ts_smartdb/server/handlers/index.js +10 -0
  56. package/dist_ts/ts_smartdb/server/index.d.ts +6 -0
  57. package/dist_ts/ts_smartdb/server/index.js +7 -0
  58. package/dist_ts/ts_smartdb/storage/FileStorageAdapter.d.ts +85 -0
  59. package/dist_ts/ts_smartdb/storage/FileStorageAdapter.js +465 -0
  60. package/dist_ts/ts_smartdb/storage/IStorageAdapter.d.ts +145 -0
  61. package/dist_ts/ts_smartdb/storage/IStorageAdapter.js +2 -0
  62. package/dist_ts/ts_smartdb/storage/MemoryStorageAdapter.d.ts +67 -0
  63. package/dist_ts/ts_smartdb/storage/MemoryStorageAdapter.js +378 -0
  64. package/dist_ts/ts_smartdb/storage/OpLog.d.ts +93 -0
  65. package/dist_ts/ts_smartdb/storage/OpLog.js +221 -0
  66. package/dist_ts/ts_smartdb/storage/WAL.d.ts +117 -0
  67. package/dist_ts/ts_smartdb/storage/WAL.js +286 -0
  68. package/dist_ts/ts_smartdb/types/interfaces.d.ts +363 -0
  69. package/dist_ts/ts_smartdb/types/interfaces.js +2 -0
  70. package/dist_ts/ts_smartdb/utils/checksum.d.ts +30 -0
  71. package/dist_ts/ts_smartdb/utils/checksum.js +77 -0
  72. package/dist_ts/ts_smartdb/utils/index.d.ts +1 -0
  73. package/dist_ts/ts_smartdb/utils/index.js +2 -0
  74. package/license +19 -0
  75. package/package.json +69 -0
  76. package/readme.md +529 -0
  77. package/ts/00_commitinfo_data.ts +8 -0
  78. package/ts/index.ts +11 -0
  79. package/ts/ts_local/classes.localsmartdb.ts +143 -0
  80. package/ts/ts_local/index.ts +2 -0
  81. package/ts/ts_local/plugins.ts +3 -0
  82. package/ts/ts_smartdb/engine/AggregationEngine.ts +283 -0
  83. package/ts/ts_smartdb/engine/IndexEngine.ts +798 -0
  84. package/ts/ts_smartdb/engine/QueryEngine.ts +301 -0
  85. package/ts/ts_smartdb/engine/QueryPlanner.ts +393 -0
  86. package/ts/ts_smartdb/engine/SessionEngine.ts +292 -0
  87. package/ts/ts_smartdb/engine/TransactionEngine.ts +351 -0
  88. package/ts/ts_smartdb/engine/UpdateEngine.ts +506 -0
  89. package/ts/ts_smartdb/errors/SmartdbErrors.ts +181 -0
  90. package/ts/ts_smartdb/index.ts +46 -0
  91. package/ts/ts_smartdb/plugins.ts +17 -0
  92. package/ts/ts_smartdb/server/CommandRouter.ts +289 -0
  93. package/ts/ts_smartdb/server/SmartdbServer.ts +354 -0
  94. package/ts/ts_smartdb/server/WireProtocol.ts +416 -0
  95. package/ts/ts_smartdb/server/handlers/AdminHandler.ts +719 -0
  96. package/ts/ts_smartdb/server/handlers/AggregateHandler.ts +342 -0
  97. package/ts/ts_smartdb/server/handlers/DeleteHandler.ts +115 -0
  98. package/ts/ts_smartdb/server/handlers/FindHandler.ts +330 -0
  99. package/ts/ts_smartdb/server/handlers/HelloHandler.ts +78 -0
  100. package/ts/ts_smartdb/server/handlers/IndexHandler.ts +207 -0
  101. package/ts/ts_smartdb/server/handlers/InsertHandler.ts +97 -0
  102. package/ts/ts_smartdb/server/handlers/UpdateHandler.ts +344 -0
  103. package/ts/ts_smartdb/server/handlers/index.ts +10 -0
  104. package/ts/ts_smartdb/server/index.ts +10 -0
  105. package/ts/ts_smartdb/storage/FileStorageAdapter.ts +562 -0
  106. package/ts/ts_smartdb/storage/IStorageAdapter.ts +208 -0
  107. package/ts/ts_smartdb/storage/MemoryStorageAdapter.ts +455 -0
  108. package/ts/ts_smartdb/storage/OpLog.ts +282 -0
  109. package/ts/ts_smartdb/storage/WAL.ts +375 -0
  110. package/ts/ts_smartdb/types/interfaces.ts +433 -0
  111. package/ts/ts_smartdb/utils/checksum.ts +88 -0
  112. package/ts/ts_smartdb/utils/index.ts +1 -0
@@ -0,0 +1,292 @@
1
+ import * as plugins from '../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
+ }
@@ -0,0 +1,351 @@
1
+ import * as plugins from '../plugins.js';
2
+ import type { IStorageAdapter } from '../storage/IStorageAdapter.js';
3
+ import type { Document, IStoredDocument, ITransactionOptions } from '../types/interfaces.js';
4
+ import { SmartdbTransactionError, SmartdbWriteConflictError } from '../errors/SmartdbErrors.js';
5
+
6
+ /**
7
+ * Transaction state
8
+ */
9
+ export interface ITransactionState {
10
+ id: string;
11
+ sessionId: string;
12
+ startTime: plugins.bson.Timestamp;
13
+ status: 'active' | 'committed' | 'aborted';
14
+ readSet: Map<string, Set<string>>; // ns -> document _ids read
15
+ writeSet: Map<string, Map<string, { op: 'insert' | 'update' | 'delete'; doc?: IStoredDocument; originalDoc?: IStoredDocument }>>; // ns -> _id -> operation
16
+ snapshots: Map<string, IStoredDocument[]>; // ns -> snapshot of documents
17
+ }
18
+
19
+ /**
20
+ * Transaction engine for ACID transaction support
21
+ */
22
+ export class TransactionEngine {
23
+ private storage: IStorageAdapter;
24
+ private transactions: Map<string, ITransactionState> = new Map();
25
+ private txnCounter = 0;
26
+
27
+ constructor(storage: IStorageAdapter) {
28
+ this.storage = storage;
29
+ }
30
+
31
+ /**
32
+ * Start a new transaction
33
+ */
34
+ startTransaction(sessionId: string, options?: ITransactionOptions): string {
35
+ this.txnCounter++;
36
+ const txnId = `txn_${sessionId}_${this.txnCounter}`;
37
+
38
+ const transaction: ITransactionState = {
39
+ id: txnId,
40
+ sessionId,
41
+ startTime: new plugins.bson.Timestamp({ t: Math.floor(Date.now() / 1000), i: this.txnCounter }),
42
+ status: 'active',
43
+ readSet: new Map(),
44
+ writeSet: new Map(),
45
+ snapshots: new Map(),
46
+ };
47
+
48
+ this.transactions.set(txnId, transaction);
49
+ return txnId;
50
+ }
51
+
52
+ /**
53
+ * Get a transaction by ID
54
+ */
55
+ getTransaction(txnId: string): ITransactionState | undefined {
56
+ return this.transactions.get(txnId);
57
+ }
58
+
59
+ /**
60
+ * Check if a transaction is active
61
+ */
62
+ isActive(txnId: string): boolean {
63
+ const txn = this.transactions.get(txnId);
64
+ return txn?.status === 'active';
65
+ }
66
+
67
+ /**
68
+ * Get or create a snapshot for a namespace
69
+ */
70
+ async getSnapshot(txnId: string, dbName: string, collName: string): Promise<IStoredDocument[]> {
71
+ const txn = this.transactions.get(txnId);
72
+ if (!txn || txn.status !== 'active') {
73
+ throw new SmartdbTransactionError('Transaction is not active');
74
+ }
75
+
76
+ const ns = `${dbName}.${collName}`;
77
+ if (!txn.snapshots.has(ns)) {
78
+ const snapshot = await this.storage.createSnapshot(dbName, collName);
79
+ txn.snapshots.set(ns, snapshot);
80
+ }
81
+
82
+ // Apply transaction writes to snapshot
83
+ const snapshot = txn.snapshots.get(ns)!;
84
+ const writes = txn.writeSet.get(ns);
85
+
86
+ if (!writes) {
87
+ return snapshot;
88
+ }
89
+
90
+ // Create a modified view of the snapshot
91
+ const result: IStoredDocument[] = [];
92
+ const deletedIds = new Set<string>();
93
+ const modifiedDocs = new Map<string, IStoredDocument>();
94
+
95
+ for (const [idStr, write] of writes) {
96
+ if (write.op === 'delete') {
97
+ deletedIds.add(idStr);
98
+ } else if (write.op === 'update' || write.op === 'insert') {
99
+ if (write.doc) {
100
+ modifiedDocs.set(idStr, write.doc);
101
+ }
102
+ }
103
+ }
104
+
105
+ // Add existing documents (not deleted, possibly modified)
106
+ for (const doc of snapshot) {
107
+ const idStr = doc._id.toHexString();
108
+ if (deletedIds.has(idStr)) {
109
+ continue;
110
+ }
111
+ if (modifiedDocs.has(idStr)) {
112
+ result.push(modifiedDocs.get(idStr)!);
113
+ modifiedDocs.delete(idStr);
114
+ } else {
115
+ result.push(doc);
116
+ }
117
+ }
118
+
119
+ // Add new documents (inserts)
120
+ for (const doc of modifiedDocs.values()) {
121
+ result.push(doc);
122
+ }
123
+
124
+ return result;
125
+ }
126
+
127
+ /**
128
+ * Record a read operation
129
+ */
130
+ recordRead(txnId: string, dbName: string, collName: string, docIds: string[]): void {
131
+ const txn = this.transactions.get(txnId);
132
+ if (!txn || txn.status !== 'active') return;
133
+
134
+ const ns = `${dbName}.${collName}`;
135
+ if (!txn.readSet.has(ns)) {
136
+ txn.readSet.set(ns, new Set());
137
+ }
138
+
139
+ const readSet = txn.readSet.get(ns)!;
140
+ for (const id of docIds) {
141
+ readSet.add(id);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Record a write operation (insert)
147
+ */
148
+ recordInsert(txnId: string, dbName: string, collName: string, doc: IStoredDocument): void {
149
+ const txn = this.transactions.get(txnId);
150
+ if (!txn || txn.status !== 'active') {
151
+ throw new SmartdbTransactionError('Transaction is not active');
152
+ }
153
+
154
+ const ns = `${dbName}.${collName}`;
155
+ if (!txn.writeSet.has(ns)) {
156
+ txn.writeSet.set(ns, new Map());
157
+ }
158
+
159
+ txn.writeSet.get(ns)!.set(doc._id.toHexString(), {
160
+ op: 'insert',
161
+ doc,
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Record a write operation (update)
167
+ */
168
+ recordUpdate(
169
+ txnId: string,
170
+ dbName: string,
171
+ collName: string,
172
+ originalDoc: IStoredDocument,
173
+ updatedDoc: IStoredDocument
174
+ ): void {
175
+ const txn = this.transactions.get(txnId);
176
+ if (!txn || txn.status !== 'active') {
177
+ throw new SmartdbTransactionError('Transaction is not active');
178
+ }
179
+
180
+ const ns = `${dbName}.${collName}`;
181
+ if (!txn.writeSet.has(ns)) {
182
+ txn.writeSet.set(ns, new Map());
183
+ }
184
+
185
+ const idStr = originalDoc._id.toHexString();
186
+ const existing = txn.writeSet.get(ns)!.get(idStr);
187
+
188
+ // If we already have a write for this document, update it
189
+ if (existing) {
190
+ existing.doc = updatedDoc;
191
+ } else {
192
+ txn.writeSet.get(ns)!.set(idStr, {
193
+ op: 'update',
194
+ doc: updatedDoc,
195
+ originalDoc,
196
+ });
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Record a write operation (delete)
202
+ */
203
+ recordDelete(txnId: string, dbName: string, collName: string, doc: IStoredDocument): void {
204
+ const txn = this.transactions.get(txnId);
205
+ if (!txn || txn.status !== 'active') {
206
+ throw new SmartdbTransactionError('Transaction is not active');
207
+ }
208
+
209
+ const ns = `${dbName}.${collName}`;
210
+ if (!txn.writeSet.has(ns)) {
211
+ txn.writeSet.set(ns, new Map());
212
+ }
213
+
214
+ const idStr = doc._id.toHexString();
215
+ const existing = txn.writeSet.get(ns)!.get(idStr);
216
+
217
+ if (existing && existing.op === 'insert') {
218
+ // If we inserted and then deleted, just remove the write
219
+ txn.writeSet.get(ns)!.delete(idStr);
220
+ } else {
221
+ txn.writeSet.get(ns)!.set(idStr, {
222
+ op: 'delete',
223
+ originalDoc: doc,
224
+ });
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Commit a transaction
230
+ */
231
+ async commitTransaction(txnId: string): Promise<void> {
232
+ const txn = this.transactions.get(txnId);
233
+ if (!txn) {
234
+ throw new SmartdbTransactionError('Transaction not found');
235
+ }
236
+ if (txn.status !== 'active') {
237
+ throw new SmartdbTransactionError(`Cannot commit transaction in state: ${txn.status}`);
238
+ }
239
+
240
+ // Check for write conflicts
241
+ for (const [ns, writes] of txn.writeSet) {
242
+ const [dbName, collName] = ns.split('.');
243
+ const ids = Array.from(writes.keys()).map(id => new plugins.bson.ObjectId(id));
244
+
245
+ const hasConflicts = await this.storage.hasConflicts(dbName, collName, ids, txn.startTime);
246
+ if (hasConflicts) {
247
+ txn.status = 'aborted';
248
+ throw new SmartdbWriteConflictError();
249
+ }
250
+ }
251
+
252
+ // Apply all writes
253
+ for (const [ns, writes] of txn.writeSet) {
254
+ const [dbName, collName] = ns.split('.');
255
+
256
+ for (const [idStr, write] of writes) {
257
+ switch (write.op) {
258
+ case 'insert':
259
+ if (write.doc) {
260
+ await this.storage.insertOne(dbName, collName, write.doc);
261
+ }
262
+ break;
263
+ case 'update':
264
+ if (write.doc) {
265
+ await this.storage.updateById(dbName, collName, new plugins.bson.ObjectId(idStr), write.doc);
266
+ }
267
+ break;
268
+ case 'delete':
269
+ await this.storage.deleteById(dbName, collName, new plugins.bson.ObjectId(idStr));
270
+ break;
271
+ }
272
+ }
273
+ }
274
+
275
+ txn.status = 'committed';
276
+ }
277
+
278
+ /**
279
+ * Abort a transaction
280
+ */
281
+ async abortTransaction(txnId: string): Promise<void> {
282
+ const txn = this.transactions.get(txnId);
283
+ if (!txn) {
284
+ throw new SmartdbTransactionError('Transaction not found');
285
+ }
286
+ if (txn.status !== 'active') {
287
+ // Already committed or aborted, just return
288
+ return;
289
+ }
290
+
291
+ // Simply discard all buffered writes
292
+ txn.writeSet.clear();
293
+ txn.readSet.clear();
294
+ txn.snapshots.clear();
295
+ txn.status = 'aborted';
296
+ }
297
+
298
+ /**
299
+ * End a transaction (cleanup)
300
+ */
301
+ endTransaction(txnId: string): void {
302
+ this.transactions.delete(txnId);
303
+ }
304
+
305
+ /**
306
+ * Get all pending writes for a namespace
307
+ */
308
+ getPendingWrites(txnId: string, dbName: string, collName: string): Map<string, { op: 'insert' | 'update' | 'delete'; doc?: IStoredDocument }> | undefined {
309
+ const txn = this.transactions.get(txnId);
310
+ if (!txn) return undefined;
311
+
312
+ const ns = `${dbName}.${collName}`;
313
+ return txn.writeSet.get(ns);
314
+ }
315
+
316
+ /**
317
+ * Execute a callback within a transaction, with automatic retry on conflict
318
+ */
319
+ async withTransaction<T>(
320
+ sessionId: string,
321
+ callback: (txnId: string) => Promise<T>,
322
+ options?: ITransactionOptions & { maxRetries?: number }
323
+ ): Promise<T> {
324
+ const maxRetries = options?.maxRetries ?? 3;
325
+ let lastError: Error | undefined;
326
+
327
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
328
+ const txnId = this.startTransaction(sessionId, options);
329
+
330
+ try {
331
+ const result = await callback(txnId);
332
+ await this.commitTransaction(txnId);
333
+ this.endTransaction(txnId);
334
+ return result;
335
+ } catch (error: any) {
336
+ await this.abortTransaction(txnId);
337
+ this.endTransaction(txnId);
338
+
339
+ if (error instanceof SmartdbWriteConflictError && attempt < maxRetries - 1) {
340
+ // Retry on write conflict
341
+ lastError = error;
342
+ continue;
343
+ }
344
+
345
+ throw error;
346
+ }
347
+ }
348
+
349
+ throw lastError || new SmartdbTransactionError('Transaction failed after max retries');
350
+ }
351
+ }