@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,208 @@
1
+ import type * as plugins from '../plugins.js';
2
+ import type { IStoredDocument, IOpLogEntry, Document } from '../types/interfaces.js';
3
+
4
+ /**
5
+ * Storage adapter interface for SmartDB
6
+ * Implementations can provide different storage backends (memory, file, etc.)
7
+ */
8
+ export interface IStorageAdapter {
9
+ /**
10
+ * Initialize the storage adapter
11
+ */
12
+ initialize(): Promise<void>;
13
+
14
+ /**
15
+ * Close the storage adapter and release resources
16
+ */
17
+ close(): Promise<void>;
18
+
19
+ // ============================================================================
20
+ // Database Operations
21
+ // ============================================================================
22
+
23
+ /**
24
+ * List all database names
25
+ */
26
+ listDatabases(): Promise<string[]>;
27
+
28
+ /**
29
+ * Create a database (typically lazy - just marks it as existing)
30
+ */
31
+ createDatabase(dbName: string): Promise<void>;
32
+
33
+ /**
34
+ * Drop a database and all its collections
35
+ */
36
+ dropDatabase(dbName: string): Promise<boolean>;
37
+
38
+ /**
39
+ * Check if a database exists
40
+ */
41
+ databaseExists(dbName: string): Promise<boolean>;
42
+
43
+ // ============================================================================
44
+ // Collection Operations
45
+ // ============================================================================
46
+
47
+ /**
48
+ * List all collection names in a database
49
+ */
50
+ listCollections(dbName: string): Promise<string[]>;
51
+
52
+ /**
53
+ * Create a collection
54
+ */
55
+ createCollection(dbName: string, collName: string): Promise<void>;
56
+
57
+ /**
58
+ * Drop a collection
59
+ */
60
+ dropCollection(dbName: string, collName: string): Promise<boolean>;
61
+
62
+ /**
63
+ * Check if a collection exists
64
+ */
65
+ collectionExists(dbName: string, collName: string): Promise<boolean>;
66
+
67
+ /**
68
+ * Rename a collection
69
+ */
70
+ renameCollection(dbName: string, oldName: string, newName: string): Promise<void>;
71
+
72
+ // ============================================================================
73
+ // Document Operations
74
+ // ============================================================================
75
+
76
+ /**
77
+ * Insert a single document
78
+ * @returns The inserted document with _id
79
+ */
80
+ insertOne(dbName: string, collName: string, doc: Document): Promise<IStoredDocument>;
81
+
82
+ /**
83
+ * Insert multiple documents
84
+ * @returns Array of inserted documents with _ids
85
+ */
86
+ insertMany(dbName: string, collName: string, docs: Document[]): Promise<IStoredDocument[]>;
87
+
88
+ /**
89
+ * Find all documents in a collection
90
+ */
91
+ findAll(dbName: string, collName: string): Promise<IStoredDocument[]>;
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
+
99
+ /**
100
+ * Find a document by _id
101
+ */
102
+ findById(dbName: string, collName: string, id: plugins.bson.ObjectId): Promise<IStoredDocument | null>;
103
+
104
+ /**
105
+ * Update a document by _id
106
+ * @returns true if document was updated
107
+ */
108
+ updateById(dbName: string, collName: string, id: plugins.bson.ObjectId, doc: IStoredDocument): Promise<boolean>;
109
+
110
+ /**
111
+ * Delete a document by _id
112
+ * @returns true if document was deleted
113
+ */
114
+ deleteById(dbName: string, collName: string, id: plugins.bson.ObjectId): Promise<boolean>;
115
+
116
+ /**
117
+ * Delete multiple documents by _id
118
+ * @returns Number of deleted documents
119
+ */
120
+ deleteByIds(dbName: string, collName: string, ids: plugins.bson.ObjectId[]): Promise<number>;
121
+
122
+ /**
123
+ * Get the count of documents in a collection
124
+ */
125
+ count(dbName: string, collName: string): Promise<number>;
126
+
127
+ // ============================================================================
128
+ // Index Operations
129
+ // ============================================================================
130
+
131
+ /**
132
+ * Store index metadata
133
+ */
134
+ saveIndex(
135
+ dbName: string,
136
+ collName: string,
137
+ indexName: string,
138
+ indexSpec: { key: Record<string, any>; unique?: boolean; sparse?: boolean; expireAfterSeconds?: number }
139
+ ): Promise<void>;
140
+
141
+ /**
142
+ * Get all index metadata for a collection
143
+ */
144
+ getIndexes(dbName: string, collName: string): Promise<Array<{
145
+ name: string;
146
+ key: Record<string, any>;
147
+ unique?: boolean;
148
+ sparse?: boolean;
149
+ expireAfterSeconds?: number;
150
+ }>>;
151
+
152
+ /**
153
+ * Drop an index
154
+ */
155
+ dropIndex(dbName: string, collName: string, indexName: string): Promise<boolean>;
156
+
157
+ // ============================================================================
158
+ // OpLog Operations (for change streams)
159
+ // ============================================================================
160
+
161
+ /**
162
+ * Append an operation to the oplog
163
+ */
164
+ appendOpLog(entry: IOpLogEntry): Promise<void>;
165
+
166
+ /**
167
+ * Get oplog entries after a timestamp
168
+ */
169
+ getOpLogAfter(ts: plugins.bson.Timestamp, limit?: number): Promise<IOpLogEntry[]>;
170
+
171
+ /**
172
+ * Get the latest oplog timestamp
173
+ */
174
+ getLatestOpLogTimestamp(): Promise<plugins.bson.Timestamp | null>;
175
+
176
+ // ============================================================================
177
+ // Transaction Support
178
+ // ============================================================================
179
+
180
+ /**
181
+ * Create a snapshot of current data for transaction isolation
182
+ */
183
+ createSnapshot(dbName: string, collName: string): Promise<IStoredDocument[]>;
184
+
185
+ /**
186
+ * Check if any documents have been modified since the snapshot
187
+ */
188
+ hasConflicts(
189
+ dbName: string,
190
+ collName: string,
191
+ ids: plugins.bson.ObjectId[],
192
+ snapshotTime: plugins.bson.Timestamp
193
+ ): Promise<boolean>;
194
+
195
+ // ============================================================================
196
+ // Persistence (optional, for MemoryStorageAdapter with file backup)
197
+ // ============================================================================
198
+
199
+ /**
200
+ * Persist current state to disk (if supported)
201
+ */
202
+ persist?(): Promise<void>;
203
+
204
+ /**
205
+ * Load state from disk (if supported)
206
+ */
207
+ restore?(): Promise<void>;
208
+ }
@@ -0,0 +1,455 @@
1
+ import * as plugins from '../plugins.js';
2
+ import type { IStorageAdapter } from './IStorageAdapter.js';
3
+ import type { IStoredDocument, IOpLogEntry, Document } from '../types/interfaces.js';
4
+
5
+ /**
6
+ * In-memory storage adapter for SmartDB
7
+ * Optionally supports persistence to a file
8
+ */
9
+ export class MemoryStorageAdapter implements IStorageAdapter {
10
+ // Database -> Collection -> Documents
11
+ private databases: Map<string, Map<string, Map<string, IStoredDocument>>> = new Map();
12
+
13
+ // Database -> Collection -> Indexes
14
+ private indexes: Map<string, Map<string, Array<{
15
+ name: string;
16
+ key: Record<string, any>;
17
+ unique?: boolean;
18
+ sparse?: boolean;
19
+ expireAfterSeconds?: number;
20
+ }>>> = new Map();
21
+
22
+ // OpLog entries
23
+ private opLog: IOpLogEntry[] = [];
24
+ private opLogCounter = 0;
25
+
26
+ // Persistence settings
27
+ private persistPath?: string;
28
+ private persistInterval?: ReturnType<typeof setInterval>;
29
+ private fs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
30
+
31
+ constructor(options?: { persistPath?: string; persistIntervalMs?: number }) {
32
+ this.persistPath = options?.persistPath;
33
+ if (this.persistPath && options?.persistIntervalMs) {
34
+ this.persistInterval = setInterval(() => {
35
+ this.persist().catch(console.error);
36
+ }, options.persistIntervalMs);
37
+ }
38
+ }
39
+
40
+ async initialize(): Promise<void> {
41
+ if (this.persistPath) {
42
+ await this.restore();
43
+ }
44
+ }
45
+
46
+ async close(): Promise<void> {
47
+ if (this.persistInterval) {
48
+ clearInterval(this.persistInterval);
49
+ }
50
+ if (this.persistPath) {
51
+ await this.persist();
52
+ }
53
+ }
54
+
55
+ // ============================================================================
56
+ // Database Operations
57
+ // ============================================================================
58
+
59
+ async listDatabases(): Promise<string[]> {
60
+ return Array.from(this.databases.keys());
61
+ }
62
+
63
+ async createDatabase(dbName: string): Promise<void> {
64
+ if (!this.databases.has(dbName)) {
65
+ this.databases.set(dbName, new Map());
66
+ this.indexes.set(dbName, new Map());
67
+ }
68
+ }
69
+
70
+ async dropDatabase(dbName: string): Promise<boolean> {
71
+ const existed = this.databases.has(dbName);
72
+ this.databases.delete(dbName);
73
+ this.indexes.delete(dbName);
74
+ return existed;
75
+ }
76
+
77
+ async databaseExists(dbName: string): Promise<boolean> {
78
+ return this.databases.has(dbName);
79
+ }
80
+
81
+ // ============================================================================
82
+ // Collection Operations
83
+ // ============================================================================
84
+
85
+ async listCollections(dbName: string): Promise<string[]> {
86
+ const db = this.databases.get(dbName);
87
+ return db ? Array.from(db.keys()) : [];
88
+ }
89
+
90
+ async createCollection(dbName: string, collName: string): Promise<void> {
91
+ await this.createDatabase(dbName);
92
+ const db = this.databases.get(dbName)!;
93
+ if (!db.has(collName)) {
94
+ db.set(collName, new Map());
95
+ // Initialize default _id index
96
+ const dbIndexes = this.indexes.get(dbName)!;
97
+ dbIndexes.set(collName, [{ name: '_id_', key: { _id: 1 }, unique: true }]);
98
+ }
99
+ }
100
+
101
+ async dropCollection(dbName: string, collName: string): Promise<boolean> {
102
+ const db = this.databases.get(dbName);
103
+ if (!db) return false;
104
+ const existed = db.has(collName);
105
+ db.delete(collName);
106
+ const dbIndexes = this.indexes.get(dbName);
107
+ if (dbIndexes) {
108
+ dbIndexes.delete(collName);
109
+ }
110
+ return existed;
111
+ }
112
+
113
+ async collectionExists(dbName: string, collName: string): Promise<boolean> {
114
+ const db = this.databases.get(dbName);
115
+ return db ? db.has(collName) : false;
116
+ }
117
+
118
+ async renameCollection(dbName: string, oldName: string, newName: string): Promise<void> {
119
+ const db = this.databases.get(dbName);
120
+ if (!db || !db.has(oldName)) {
121
+ throw new Error(`Collection ${oldName} not found`);
122
+ }
123
+ const collection = db.get(oldName)!;
124
+ db.set(newName, collection);
125
+ db.delete(oldName);
126
+
127
+ // Also rename indexes
128
+ const dbIndexes = this.indexes.get(dbName);
129
+ if (dbIndexes && dbIndexes.has(oldName)) {
130
+ const collIndexes = dbIndexes.get(oldName)!;
131
+ dbIndexes.set(newName, collIndexes);
132
+ dbIndexes.delete(oldName);
133
+ }
134
+ }
135
+
136
+ // ============================================================================
137
+ // Document Operations
138
+ // ============================================================================
139
+
140
+ private getCollection(dbName: string, collName: string): Map<string, IStoredDocument> {
141
+ const db = this.databases.get(dbName);
142
+ if (!db) {
143
+ throw new Error(`Database ${dbName} not found`);
144
+ }
145
+ const collection = db.get(collName);
146
+ if (!collection) {
147
+ throw new Error(`Collection ${collName} not found`);
148
+ }
149
+ return collection;
150
+ }
151
+
152
+ private ensureCollection(dbName: string, collName: string): Map<string, IStoredDocument> {
153
+ if (!this.databases.has(dbName)) {
154
+ this.databases.set(dbName, new Map());
155
+ this.indexes.set(dbName, new Map());
156
+ }
157
+ const db = this.databases.get(dbName)!;
158
+ if (!db.has(collName)) {
159
+ db.set(collName, new Map());
160
+ const dbIndexes = this.indexes.get(dbName)!;
161
+ dbIndexes.set(collName, [{ name: '_id_', key: { _id: 1 }, unique: true }]);
162
+ }
163
+ return db.get(collName)!;
164
+ }
165
+
166
+ async insertOne(dbName: string, collName: string, doc: Document): Promise<IStoredDocument> {
167
+ const collection = this.ensureCollection(dbName, collName);
168
+ const storedDoc: IStoredDocument = {
169
+ ...doc,
170
+ _id: doc._id instanceof plugins.bson.ObjectId ? doc._id : new plugins.bson.ObjectId(doc._id),
171
+ };
172
+
173
+ if (!storedDoc._id) {
174
+ storedDoc._id = new plugins.bson.ObjectId();
175
+ }
176
+
177
+ const idStr = storedDoc._id.toHexString();
178
+ if (collection.has(idStr)) {
179
+ throw new Error(`Duplicate key error: _id ${idStr}`);
180
+ }
181
+
182
+ collection.set(idStr, storedDoc);
183
+ return storedDoc;
184
+ }
185
+
186
+ async insertMany(dbName: string, collName: string, docs: Document[]): Promise<IStoredDocument[]> {
187
+ const results: IStoredDocument[] = [];
188
+ for (const doc of docs) {
189
+ results.push(await this.insertOne(dbName, collName, doc));
190
+ }
191
+ return results;
192
+ }
193
+
194
+ async findAll(dbName: string, collName: string): Promise<IStoredDocument[]> {
195
+ const collection = this.ensureCollection(dbName, collName);
196
+ return Array.from(collection.values());
197
+ }
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
+
211
+ async findById(dbName: string, collName: string, id: plugins.bson.ObjectId): Promise<IStoredDocument | null> {
212
+ const collection = this.ensureCollection(dbName, collName);
213
+ return collection.get(id.toHexString()) || null;
214
+ }
215
+
216
+ async updateById(dbName: string, collName: string, id: plugins.bson.ObjectId, doc: IStoredDocument): Promise<boolean> {
217
+ const collection = this.ensureCollection(dbName, collName);
218
+ const idStr = id.toHexString();
219
+ if (!collection.has(idStr)) {
220
+ return false;
221
+ }
222
+ collection.set(idStr, doc);
223
+ return true;
224
+ }
225
+
226
+ async deleteById(dbName: string, collName: string, id: plugins.bson.ObjectId): Promise<boolean> {
227
+ const collection = this.ensureCollection(dbName, collName);
228
+ return collection.delete(id.toHexString());
229
+ }
230
+
231
+ async deleteByIds(dbName: string, collName: string, ids: plugins.bson.ObjectId[]): Promise<number> {
232
+ let count = 0;
233
+ for (const id of ids) {
234
+ if (await this.deleteById(dbName, collName, id)) {
235
+ count++;
236
+ }
237
+ }
238
+ return count;
239
+ }
240
+
241
+ async count(dbName: string, collName: string): Promise<number> {
242
+ const collection = this.ensureCollection(dbName, collName);
243
+ return collection.size;
244
+ }
245
+
246
+ // ============================================================================
247
+ // Index Operations
248
+ // ============================================================================
249
+
250
+ async saveIndex(
251
+ dbName: string,
252
+ collName: string,
253
+ indexName: string,
254
+ indexSpec: { key: Record<string, any>; unique?: boolean; sparse?: boolean; expireAfterSeconds?: number }
255
+ ): Promise<void> {
256
+ await this.createCollection(dbName, collName);
257
+ const dbIndexes = this.indexes.get(dbName)!;
258
+ let collIndexes = dbIndexes.get(collName);
259
+ if (!collIndexes) {
260
+ collIndexes = [{ name: '_id_', key: { _id: 1 }, unique: true }];
261
+ dbIndexes.set(collName, collIndexes);
262
+ }
263
+
264
+ // Check if index already exists
265
+ const existingIndex = collIndexes.findIndex(i => i.name === indexName);
266
+ if (existingIndex >= 0) {
267
+ collIndexes[existingIndex] = { name: indexName, ...indexSpec };
268
+ } else {
269
+ collIndexes.push({ name: indexName, ...indexSpec });
270
+ }
271
+ }
272
+
273
+ async getIndexes(dbName: string, collName: string): Promise<Array<{
274
+ name: string;
275
+ key: Record<string, any>;
276
+ unique?: boolean;
277
+ sparse?: boolean;
278
+ expireAfterSeconds?: number;
279
+ }>> {
280
+ const dbIndexes = this.indexes.get(dbName);
281
+ if (!dbIndexes) return [{ name: '_id_', key: { _id: 1 }, unique: true }];
282
+ const collIndexes = dbIndexes.get(collName);
283
+ return collIndexes || [{ name: '_id_', key: { _id: 1 }, unique: true }];
284
+ }
285
+
286
+ async dropIndex(dbName: string, collName: string, indexName: string): Promise<boolean> {
287
+ if (indexName === '_id_') {
288
+ throw new Error('Cannot drop _id index');
289
+ }
290
+ const dbIndexes = this.indexes.get(dbName);
291
+ if (!dbIndexes) return false;
292
+ const collIndexes = dbIndexes.get(collName);
293
+ if (!collIndexes) return false;
294
+
295
+ const idx = collIndexes.findIndex(i => i.name === indexName);
296
+ if (idx >= 0) {
297
+ collIndexes.splice(idx, 1);
298
+ return true;
299
+ }
300
+ return false;
301
+ }
302
+
303
+ // ============================================================================
304
+ // OpLog Operations
305
+ // ============================================================================
306
+
307
+ async appendOpLog(entry: IOpLogEntry): Promise<void> {
308
+ this.opLog.push(entry);
309
+ // Trim oplog if it gets too large (keep last 10000 entries)
310
+ if (this.opLog.length > 10000) {
311
+ this.opLog = this.opLog.slice(-10000);
312
+ }
313
+ }
314
+
315
+ async getOpLogAfter(ts: plugins.bson.Timestamp, limit: number = 1000): Promise<IOpLogEntry[]> {
316
+ const tsValue = ts.toNumber();
317
+ const entries = this.opLog.filter(e => e.ts.toNumber() > tsValue);
318
+ return entries.slice(0, limit);
319
+ }
320
+
321
+ async getLatestOpLogTimestamp(): Promise<plugins.bson.Timestamp | null> {
322
+ if (this.opLog.length === 0) return null;
323
+ return this.opLog[this.opLog.length - 1].ts;
324
+ }
325
+
326
+ /**
327
+ * Generate a new timestamp for oplog entries
328
+ */
329
+ generateTimestamp(): plugins.bson.Timestamp {
330
+ this.opLogCounter++;
331
+ return new plugins.bson.Timestamp({ t: Math.floor(Date.now() / 1000), i: this.opLogCounter });
332
+ }
333
+
334
+ // ============================================================================
335
+ // Transaction Support
336
+ // ============================================================================
337
+
338
+ async createSnapshot(dbName: string, collName: string): Promise<IStoredDocument[]> {
339
+ const docs = await this.findAll(dbName, collName);
340
+ // Deep clone the documents for snapshot isolation
341
+ return docs.map(doc => JSON.parse(JSON.stringify(doc)));
342
+ }
343
+
344
+ async hasConflicts(
345
+ dbName: string,
346
+ collName: string,
347
+ ids: plugins.bson.ObjectId[],
348
+ snapshotTime: plugins.bson.Timestamp
349
+ ): Promise<boolean> {
350
+ // Check if any of the given document IDs have been modified after snapshotTime
351
+ const ns = `${dbName}.${collName}`;
352
+ const modifiedIds = new Set<string>();
353
+
354
+ for (const entry of this.opLog) {
355
+ if (entry.ts.greaterThan(snapshotTime) && entry.ns === ns) {
356
+ if (entry.o._id) {
357
+ modifiedIds.add(entry.o._id.toString());
358
+ }
359
+ if (entry.o2?._id) {
360
+ modifiedIds.add(entry.o2._id.toString());
361
+ }
362
+ }
363
+ }
364
+
365
+ for (const id of ids) {
366
+ if (modifiedIds.has(id.toString())) {
367
+ return true;
368
+ }
369
+ }
370
+
371
+ return false;
372
+ }
373
+
374
+ // ============================================================================
375
+ // Persistence
376
+ // ============================================================================
377
+
378
+ async persist(): Promise<void> {
379
+ if (!this.persistPath) return;
380
+
381
+ const data = {
382
+ databases: {} as Record<string, Record<string, IStoredDocument[]>>,
383
+ indexes: {} as Record<string, Record<string, any[]>>,
384
+ opLogCounter: this.opLogCounter,
385
+ };
386
+
387
+ for (const [dbName, collections] of this.databases) {
388
+ data.databases[dbName] = {};
389
+ for (const [collName, docs] of collections) {
390
+ data.databases[dbName][collName] = Array.from(docs.values());
391
+ }
392
+ }
393
+
394
+ for (const [dbName, collIndexes] of this.indexes) {
395
+ data.indexes[dbName] = {};
396
+ for (const [collName, indexes] of collIndexes) {
397
+ data.indexes[dbName][collName] = indexes;
398
+ }
399
+ }
400
+
401
+ // Ensure parent directory exists
402
+ const dir = this.persistPath.substring(0, this.persistPath.lastIndexOf('/'));
403
+ if (dir) {
404
+ await this.fs.directory(dir).recursive().create();
405
+ }
406
+ await this.fs.file(this.persistPath).encoding('utf8').write(JSON.stringify(data, null, 2));
407
+ }
408
+
409
+ async restore(): Promise<void> {
410
+ if (!this.persistPath) return;
411
+
412
+ try {
413
+ const exists = await this.fs.file(this.persistPath).exists();
414
+ if (!exists) return;
415
+
416
+ const content = await this.fs.file(this.persistPath).encoding('utf8').read();
417
+ const data = JSON.parse(content as string);
418
+
419
+ this.databases.clear();
420
+ this.indexes.clear();
421
+
422
+ for (const [dbName, collections] of Object.entries(data.databases || {})) {
423
+ const dbMap = new Map<string, Map<string, IStoredDocument>>();
424
+ this.databases.set(dbName, dbMap);
425
+
426
+ for (const [collName, docs] of Object.entries(collections as Record<string, any[]>)) {
427
+ const collMap = new Map<string, IStoredDocument>();
428
+ for (const doc of docs) {
429
+ // Restore ObjectId
430
+ if (doc._id && typeof doc._id === 'string') {
431
+ doc._id = new plugins.bson.ObjectId(doc._id);
432
+ } else if (doc._id && typeof doc._id === 'object' && doc._id.$oid) {
433
+ doc._id = new plugins.bson.ObjectId(doc._id.$oid);
434
+ }
435
+ collMap.set(doc._id.toHexString(), doc);
436
+ }
437
+ dbMap.set(collName, collMap);
438
+ }
439
+ }
440
+
441
+ for (const [dbName, collIndexes] of Object.entries(data.indexes || {})) {
442
+ const indexMap = new Map<string, any[]>();
443
+ this.indexes.set(dbName, indexMap);
444
+ for (const [collName, indexes] of Object.entries(collIndexes as Record<string, any[]>)) {
445
+ indexMap.set(collName, indexes);
446
+ }
447
+ }
448
+
449
+ this.opLogCounter = data.opLogCounter || 0;
450
+ } catch (error) {
451
+ // If restore fails, start fresh
452
+ console.warn('Failed to restore from persistence:', error);
453
+ }
454
+ }
455
+ }