@push.rocks/smartdb 2.1.1 → 2.4.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_rust/rustdb_linux_amd64 +0 -0
- package/dist_rust/rustdb_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/ts_local/classes.localsmartdb.js +5 -1
- package/dist_ts/ts_migration/classes.storagemigrator.d.ts +24 -0
- package/dist_ts/ts_migration/classes.storagemigrator.js +75 -0
- package/dist_ts/ts_migration/index.d.ts +1 -0
- package/dist_ts/ts_migration/index.js +2 -0
- package/dist_ts/ts_migration/migrators/v0_to_v1.d.ts +9 -0
- package/dist_ts/ts_migration/migrators/v0_to_v1.js +225 -0
- package/dist_ts/ts_smartdb/server/SmartdbServer.js +7 -1
- package/dist_ts_debugserver/bundled.js +1 -1
- package/package.json +3 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +3 -0
- package/ts/ts_local/classes.localsmartdb.ts +5 -0
- package/ts/ts_migration/classes.storagemigrator.ts +93 -0
- package/ts/ts_migration/index.ts +1 -0
- package/ts/ts_migration/migrators/v0_to_v1.ts +253 -0
- package/ts/ts_smartdb/server/SmartdbServer.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartdb",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A MongoDB-compatible embedded database server with wire protocol support, backed by a high-performance Rust engine.",
|
|
6
6
|
"exports": {
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@api.global/typedserver": "^8.0.0",
|
|
31
31
|
"@design.estate/dees-element": "^2.0.0",
|
|
32
|
-
"@push.rocks/smartrust": "^1.3.2"
|
|
32
|
+
"@push.rocks/smartrust": "^1.3.2",
|
|
33
|
+
"bson": "^7.2.0"
|
|
33
34
|
},
|
|
34
35
|
"browserslist": [
|
|
35
36
|
"last 1 chrome versions"
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/index.ts
CHANGED
|
@@ -7,6 +7,9 @@ export * from './ts_smartdb/index.js';
|
|
|
7
7
|
export { LocalSmartDb } from './ts_local/index.js';
|
|
8
8
|
export type { ILocalSmartDbOptions, ILocalSmartDbConnectionInfo } from './ts_local/index.js';
|
|
9
9
|
|
|
10
|
+
// Export migration
|
|
11
|
+
export { StorageMigrator } from './ts_migration/index.js';
|
|
12
|
+
|
|
10
13
|
// Export commitinfo
|
|
11
14
|
export { commitinfo };
|
|
12
15
|
|
|
@@ -2,6 +2,7 @@ import * as crypto from 'crypto';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import { SmartdbServer } from '../ts_smartdb/index.js';
|
|
5
|
+
import { StorageMigrator } from '../ts_migration/index.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Connection information returned by LocalSmartDb.start()
|
|
@@ -73,6 +74,10 @@ export class LocalSmartDb {
|
|
|
73
74
|
throw new Error('LocalSmartDb is already running');
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// Run storage migration before starting the Rust engine
|
|
78
|
+
const migrator = new StorageMigrator(this.options.folderPath);
|
|
79
|
+
await migrator.run();
|
|
80
|
+
|
|
76
81
|
// Use provided socket path or generate one
|
|
77
82
|
this.generatedSocketPath = this.options.socketPath ?? this.generateSocketPath();
|
|
78
83
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { migrateV0ToV1 } from './migrators/v0_to_v1.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detected storage format version.
|
|
7
|
+
* - v0: Legacy JSON format ({db}/{coll}.json files)
|
|
8
|
+
* - v1: Bitcask binary format ({db}/{coll}/data.rdb directories)
|
|
9
|
+
*/
|
|
10
|
+
type TStorageVersion = 0 | 1;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* StorageMigrator — runs before the Rust engine starts.
|
|
14
|
+
*
|
|
15
|
+
* Detects the current storage format version and runs the appropriate
|
|
16
|
+
* migration chain. The Rust engine only knows the current format (v1).
|
|
17
|
+
*
|
|
18
|
+
* Migration is safe: original files are never modified or deleted.
|
|
19
|
+
* On success, a console hint is printed about which old files can be removed.
|
|
20
|
+
*/
|
|
21
|
+
export class StorageMigrator {
|
|
22
|
+
private storagePath: string;
|
|
23
|
+
|
|
24
|
+
constructor(storagePath: string) {
|
|
25
|
+
this.storagePath = storagePath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run any needed migrations. Safe to call even if storage is already current.
|
|
30
|
+
*/
|
|
31
|
+
async run(): Promise<void> {
|
|
32
|
+
if (!fs.existsSync(this.storagePath)) {
|
|
33
|
+
return; // No data yet — nothing to migrate
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const version = this.detectVersion();
|
|
37
|
+
|
|
38
|
+
if (version === 1) {
|
|
39
|
+
return; // Already current
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (version === 0) {
|
|
43
|
+
console.log(`[smartdb] Detected v0 (JSON) storage format at ${this.storagePath}`);
|
|
44
|
+
console.log(`[smartdb] Running migration v0 → v1 (Bitcask binary format)...`);
|
|
45
|
+
|
|
46
|
+
const deletableFiles = await migrateV0ToV1(this.storagePath);
|
|
47
|
+
|
|
48
|
+
if (deletableFiles.length > 0) {
|
|
49
|
+
console.log(`[smartdb] Migration v0 → v1 complete.`);
|
|
50
|
+
console.log(`[smartdb] The following old files can be safely deleted:`);
|
|
51
|
+
for (const f of deletableFiles) {
|
|
52
|
+
console.log(`[smartdb] ${f}`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
console.log(`[smartdb] Migration v0 → v1 complete. No old files to clean up.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detect the storage format version by inspecting the directory structure.
|
|
62
|
+
*
|
|
63
|
+
* v0: {db}/{coll}.json files exist
|
|
64
|
+
* v1: {db}/{coll}/data.rdb directories exist
|
|
65
|
+
*/
|
|
66
|
+
private detectVersion(): TStorageVersion {
|
|
67
|
+
const entries = fs.readdirSync(this.storagePath, { withFileTypes: true });
|
|
68
|
+
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (!entry.isDirectory()) continue;
|
|
71
|
+
|
|
72
|
+
const dbDir = path.join(this.storagePath, entry.name);
|
|
73
|
+
const dbEntries = fs.readdirSync(dbDir, { withFileTypes: true });
|
|
74
|
+
|
|
75
|
+
for (const dbEntry of dbEntries) {
|
|
76
|
+
// v1: subdirectory with data.rdb
|
|
77
|
+
if (dbEntry.isDirectory()) {
|
|
78
|
+
const dataRdb = path.join(dbDir, dbEntry.name, 'data.rdb');
|
|
79
|
+
if (fs.existsSync(dataRdb)) {
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// v0: .json file (not .indexes.json)
|
|
84
|
+
if (dbEntry.isFile() && dbEntry.name.endsWith('.json') && !dbEntry.name.endsWith('.indexes.json')) {
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Empty or unrecognized — treat as v1 (fresh start)
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StorageMigrator } from './classes.storagemigrator.js';
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import { BSON } from 'bson';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Binary format constants (must match Rust: record.rs)
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/** File-level magic: "SMARTDB\0" */
|
|
11
|
+
const FILE_MAGIC = Buffer.from('SMARTDB\0', 'ascii');
|
|
12
|
+
/** Current format version */
|
|
13
|
+
const FORMAT_VERSION = 1;
|
|
14
|
+
/** File type tags */
|
|
15
|
+
const FILE_TYPE_DATA = 1;
|
|
16
|
+
const FILE_TYPE_HINT = 3;
|
|
17
|
+
/** File header total size */
|
|
18
|
+
const FILE_HEADER_SIZE = 64;
|
|
19
|
+
/** Per-record magic */
|
|
20
|
+
const RECORD_MAGIC = 0xDB01;
|
|
21
|
+
/** Per-record header size */
|
|
22
|
+
const RECORD_HEADER_SIZE = 22; // 2 + 8 + 4 + 4 + 4
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Binary encoding helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function writeFileHeader(fileType: number): Buffer {
|
|
29
|
+
const buf = Buffer.alloc(FILE_HEADER_SIZE, 0);
|
|
30
|
+
FILE_MAGIC.copy(buf, 0);
|
|
31
|
+
buf.writeUInt16LE(FORMAT_VERSION, 8);
|
|
32
|
+
buf.writeUInt8(fileType, 10);
|
|
33
|
+
buf.writeUInt32LE(0, 11); // flags
|
|
34
|
+
const now = BigInt(Date.now());
|
|
35
|
+
buf.writeBigUInt64LE(now, 15);
|
|
36
|
+
// bytes 23..64 are reserved (zeros)
|
|
37
|
+
return buf;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function encodeDataRecord(timestamp: bigint, key: Buffer, value: Buffer): Buffer {
|
|
41
|
+
const keyLen = key.length;
|
|
42
|
+
const valLen = value.length;
|
|
43
|
+
const totalSize = RECORD_HEADER_SIZE + keyLen + valLen;
|
|
44
|
+
const buf = Buffer.alloc(totalSize);
|
|
45
|
+
|
|
46
|
+
// Write header fields (without CRC)
|
|
47
|
+
buf.writeUInt16LE(RECORD_MAGIC, 0);
|
|
48
|
+
buf.writeBigUInt64LE(timestamp, 2);
|
|
49
|
+
buf.writeUInt32LE(keyLen, 10);
|
|
50
|
+
buf.writeUInt32LE(valLen, 14);
|
|
51
|
+
// CRC placeholder at offset 18..22 (will fill below)
|
|
52
|
+
key.copy(buf, RECORD_HEADER_SIZE);
|
|
53
|
+
value.copy(buf, RECORD_HEADER_SIZE + keyLen);
|
|
54
|
+
|
|
55
|
+
// CRC32 covers everything except the CRC field itself:
|
|
56
|
+
// bytes [0..18] + bytes [22..]
|
|
57
|
+
const crc = crc32(Buffer.concat([
|
|
58
|
+
buf.subarray(0, 18),
|
|
59
|
+
buf.subarray(22),
|
|
60
|
+
]));
|
|
61
|
+
buf.writeUInt32LE(crc, 18);
|
|
62
|
+
|
|
63
|
+
return buf;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function encodeHintEntry(key: string, offset: bigint, recordLen: number, valueLen: number, timestamp: bigint): Buffer {
|
|
67
|
+
const keyBuf = Buffer.from(key, 'utf-8');
|
|
68
|
+
const buf = Buffer.alloc(4 + keyBuf.length + 8 + 4 + 4 + 8);
|
|
69
|
+
let pos = 0;
|
|
70
|
+
buf.writeUInt32LE(keyBuf.length, pos); pos += 4;
|
|
71
|
+
keyBuf.copy(buf, pos); pos += keyBuf.length;
|
|
72
|
+
buf.writeBigUInt64LE(offset, pos); pos += 8;
|
|
73
|
+
buf.writeUInt32LE(recordLen, pos); pos += 4;
|
|
74
|
+
buf.writeUInt32LE(valueLen, pos); pos += 4;
|
|
75
|
+
buf.writeBigUInt64LE(timestamp, pos);
|
|
76
|
+
return buf;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// CRC32 (matching crc32fast in Rust)
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
const CRC32_TABLE = (() => {
|
|
84
|
+
const table = new Uint32Array(256);
|
|
85
|
+
for (let i = 0; i < 256; i++) {
|
|
86
|
+
let crc = i;
|
|
87
|
+
for (let j = 0; j < 8; j++) {
|
|
88
|
+
crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
|
|
89
|
+
}
|
|
90
|
+
table[i] = crc;
|
|
91
|
+
}
|
|
92
|
+
return table;
|
|
93
|
+
})();
|
|
94
|
+
|
|
95
|
+
function crc32(data: Buffer): number {
|
|
96
|
+
let crc = 0xFFFFFFFF;
|
|
97
|
+
for (let i = 0; i < data.length; i++) {
|
|
98
|
+
crc = CRC32_TABLE[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
|
|
99
|
+
}
|
|
100
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Migration: v0 (JSON) → v1 (Bitcask binary)
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
interface IKeyDirEntry {
|
|
108
|
+
offset: bigint;
|
|
109
|
+
recordLen: number;
|
|
110
|
+
valueLen: number;
|
|
111
|
+
timestamp: bigint;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Migrate a storage directory from v0 (JSON-per-collection) to v1 (Bitcask binary).
|
|
116
|
+
*
|
|
117
|
+
* - Original .json files are NOT modified or deleted.
|
|
118
|
+
* - New v1 files are written into {db}/{coll}/ subdirectories.
|
|
119
|
+
* - Returns a list of old files that can be safely deleted.
|
|
120
|
+
* - On failure, cleans up any partial new files and throws.
|
|
121
|
+
*/
|
|
122
|
+
export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
|
|
123
|
+
const deletableFiles: string[] = [];
|
|
124
|
+
const createdDirs: string[] = [];
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const dbEntries = fs.readdirSync(storagePath, { withFileTypes: true });
|
|
128
|
+
|
|
129
|
+
for (const dbEntry of dbEntries) {
|
|
130
|
+
if (!dbEntry.isDirectory()) continue;
|
|
131
|
+
|
|
132
|
+
const dbDir = path.join(storagePath, dbEntry.name);
|
|
133
|
+
const collFiles = fs.readdirSync(dbDir, { withFileTypes: true });
|
|
134
|
+
|
|
135
|
+
for (const collFile of collFiles) {
|
|
136
|
+
if (!collFile.isFile()) continue;
|
|
137
|
+
if (!collFile.name.endsWith('.json')) continue;
|
|
138
|
+
if (collFile.name.endsWith('.indexes.json')) continue;
|
|
139
|
+
|
|
140
|
+
const collName = collFile.name.replace(/\.json$/, '');
|
|
141
|
+
const jsonPath = path.join(dbDir, collFile.name);
|
|
142
|
+
const indexJsonPath = path.join(dbDir, `${collName}.indexes.json`);
|
|
143
|
+
|
|
144
|
+
// Target directory
|
|
145
|
+
const collDir = path.join(dbDir, collName);
|
|
146
|
+
if (fs.existsSync(collDir)) {
|
|
147
|
+
// Already migrated
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`[smartdb] Migrating ${dbEntry.name}.${collName}...`);
|
|
152
|
+
|
|
153
|
+
// Read the JSON collection
|
|
154
|
+
const jsonData = fs.readFileSync(jsonPath, 'utf-8');
|
|
155
|
+
const docs: any[] = JSON.parse(jsonData);
|
|
156
|
+
|
|
157
|
+
// Create collection directory
|
|
158
|
+
fs.mkdirSync(collDir, { recursive: true });
|
|
159
|
+
createdDirs.push(collDir);
|
|
160
|
+
|
|
161
|
+
// Write data.rdb
|
|
162
|
+
const dataPath = path.join(collDir, 'data.rdb');
|
|
163
|
+
const fd = fs.openSync(dataPath, 'w');
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
// File header
|
|
167
|
+
const headerBuf = writeFileHeader(FILE_TYPE_DATA);
|
|
168
|
+
fs.writeSync(fd, headerBuf);
|
|
169
|
+
|
|
170
|
+
let currentOffset = BigInt(FILE_HEADER_SIZE);
|
|
171
|
+
const keydir: Map<string, IKeyDirEntry> = new Map();
|
|
172
|
+
const ts = BigInt(Date.now());
|
|
173
|
+
|
|
174
|
+
for (const doc of docs) {
|
|
175
|
+
// Extract _id
|
|
176
|
+
let idHex: string;
|
|
177
|
+
if (doc._id && doc._id.$oid) {
|
|
178
|
+
idHex = doc._id.$oid;
|
|
179
|
+
} else if (typeof doc._id === 'string') {
|
|
180
|
+
idHex = doc._id;
|
|
181
|
+
} else if (doc._id) {
|
|
182
|
+
idHex = String(doc._id);
|
|
183
|
+
} else {
|
|
184
|
+
// Generate a new ObjectId
|
|
185
|
+
idHex = crypto.randomBytes(12).toString('hex');
|
|
186
|
+
doc._id = { $oid: idHex };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Serialize to BSON
|
|
190
|
+
const bsonBytes = BSON.serialize(doc);
|
|
191
|
+
const keyBuf = Buffer.from(idHex, 'utf-8');
|
|
192
|
+
const valueBuf = Buffer.from(bsonBytes);
|
|
193
|
+
|
|
194
|
+
const record = encodeDataRecord(ts, keyBuf, valueBuf);
|
|
195
|
+
fs.writeSync(fd, record);
|
|
196
|
+
|
|
197
|
+
keydir.set(idHex, {
|
|
198
|
+
offset: currentOffset,
|
|
199
|
+
recordLen: record.length,
|
|
200
|
+
valueLen: valueBuf.length,
|
|
201
|
+
timestamp: ts,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
currentOffset += BigInt(record.length);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fs.fsyncSync(fd);
|
|
208
|
+
fs.closeSync(fd);
|
|
209
|
+
|
|
210
|
+
// Write keydir.hint
|
|
211
|
+
const hintPath = path.join(collDir, 'keydir.hint');
|
|
212
|
+
const hintFd = fs.openSync(hintPath, 'w');
|
|
213
|
+
fs.writeSync(hintFd, writeFileHeader(FILE_TYPE_HINT));
|
|
214
|
+
for (const [key, entry] of keydir) {
|
|
215
|
+
fs.writeSync(hintFd, encodeHintEntry(key, entry.offset, entry.recordLen, entry.valueLen, entry.timestamp));
|
|
216
|
+
}
|
|
217
|
+
fs.fsyncSync(hintFd);
|
|
218
|
+
fs.closeSync(hintFd);
|
|
219
|
+
|
|
220
|
+
} catch (writeErr) {
|
|
221
|
+
// Clean up on write failure
|
|
222
|
+
try { fs.closeSync(fd); } catch {}
|
|
223
|
+
throw writeErr;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Copy indexes.json if it exists
|
|
227
|
+
if (fs.existsSync(indexJsonPath)) {
|
|
228
|
+
const destIndexPath = path.join(collDir, 'indexes.json');
|
|
229
|
+
fs.copyFileSync(indexJsonPath, destIndexPath);
|
|
230
|
+
deletableFiles.push(indexJsonPath);
|
|
231
|
+
} else {
|
|
232
|
+
// Write default _id index
|
|
233
|
+
const destIndexPath = path.join(collDir, 'indexes.json');
|
|
234
|
+
fs.writeFileSync(destIndexPath, JSON.stringify([{ name: '_id_', key: { _id: 1 } }], null, 2));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
deletableFiles.push(jsonPath);
|
|
238
|
+
|
|
239
|
+
console.log(`[smartdb] Migrated ${dbEntry.name}.${collName}: ${docs.length} documents`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// Clean up any partially created directories
|
|
244
|
+
for (const dir of createdDirs) {
|
|
245
|
+
try {
|
|
246
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return deletableFiles;
|
|
253
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RustDbBridge } from '../rust-db-bridge.js';
|
|
2
|
+
import { StorageMigrator } from '../../ts_migration/index.js';
|
|
2
3
|
import type {
|
|
3
4
|
IOpLogEntry,
|
|
4
5
|
IOpLogResult,
|
|
@@ -75,6 +76,12 @@ export class SmartdbServer {
|
|
|
75
76
|
throw new Error('Server is already running');
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
// Run storage migration for file-based storage before starting Rust engine
|
|
80
|
+
if (this.options.storage === 'file' && this.options.storagePath) {
|
|
81
|
+
const migrator = new StorageMigrator(this.options.storagePath);
|
|
82
|
+
await migrator.run();
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
const spawned = await this.bridge.spawn();
|
|
79
86
|
if (!spawned) {
|
|
80
87
|
throw new Error(
|