@push.rocks/smartdb 3.0.0 → 3.0.2
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/ts_migration/classes.authmetadatamigrationrunner.d.ts +2 -1
- package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.js +3 -3
- package/dist_ts/ts_migration/classes.storagemigrator.d.ts +2 -1
- package/dist_ts/ts_migration/classes.storagemigrator.js +36 -20
- package/dist_ts/ts_migration/index.d.ts +1 -0
- package/dist_ts/ts_migration/index.js +2 -1
- package/dist_ts/ts_migration/migrators/v0_to_v1.d.ts +7 -1
- package/dist_ts/ts_migration/migrators/v0_to_v1.js +162 -47
- package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.d.ts +3 -2
- package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.js +3 -3
- package/dist_ts/ts_smartdb/rust-db-bridge.d.ts +2 -2
- package/dist_ts/ts_smartdb/rust-db-bridge.js +24 -10
- package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +1 -1
- package/dist_ts/ts_smartdb/server/SmartdbServer.js +76 -9
- package/package.json +1 -1
- package/readme.md +16 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/ts_migration/classes.authmetadatamigrationrunner.ts +3 -1
- package/ts/ts_migration/classes.storagemigrator.ts +38 -19
- package/ts/ts_migration/index.ts +1 -0
- package/ts/ts_migration/migrators/v0_to_v1.ts +224 -44
- package/ts/ts_migration/migrators/v1_auth_metadata_permissions.ts +4 -0
- package/ts/ts_smartdb/rust-db-bridge.ts +32 -11
- package/ts/ts_smartdb/server/SmartdbServer.ts +89 -9
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as crypto from 'crypto';
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as crypto from 'node:crypto';
|
|
4
4
|
import { BSON } from 'bson';
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
@@ -100,6 +100,141 @@ function crc32(data: Buffer): number {
|
|
|
100
100
|
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
const pathExists = async (pathArg: string): Promise<boolean> => {
|
|
104
|
+
try {
|
|
105
|
+
await fs.stat(pathArg);
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export class SmartDbStorageMigrationCleanupError extends AggregateError {
|
|
114
|
+
constructor(operationErrorArg: unknown, cleanupErrorsArg: unknown[]) {
|
|
115
|
+
super(
|
|
116
|
+
[operationErrorArg, ...cleanupErrorsArg],
|
|
117
|
+
'SmartDB legacy storage migration failed and staging cleanup was incomplete',
|
|
118
|
+
{ cause: operationErrorArg },
|
|
119
|
+
);
|
|
120
|
+
this.name = 'SmartDbStorageMigrationCleanupError';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const runWithOwnedMigrationFileHandle = async <
|
|
125
|
+
TResult,
|
|
126
|
+
TFileHandle extends { close(): Promise<void> },
|
|
127
|
+
>(
|
|
128
|
+
fileHandleArg: TFileHandle,
|
|
129
|
+
operationArg: (
|
|
130
|
+
fileHandleArg: TFileHandle,
|
|
131
|
+
) => Promise<TResult>,
|
|
132
|
+
signalArg?: AbortSignal,
|
|
133
|
+
): Promise<TResult> => {
|
|
134
|
+
let operationResult: TResult | undefined;
|
|
135
|
+
let operationError: unknown;
|
|
136
|
+
let operationCompleted = false;
|
|
137
|
+
try {
|
|
138
|
+
operationResult = await operationArg(fileHandleArg);
|
|
139
|
+
operationCompleted = true;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
operationError = error;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
await fileHandleArg.close();
|
|
145
|
+
} catch (closeError) {
|
|
146
|
+
if (operationError !== undefined) {
|
|
147
|
+
throw new SmartDbStorageMigrationCleanupError(operationError, [closeError]);
|
|
148
|
+
}
|
|
149
|
+
if (signalArg?.aborted) {
|
|
150
|
+
throw new SmartDbStorageMigrationCleanupError(signalArg.reason, [closeError]);
|
|
151
|
+
}
|
|
152
|
+
throw closeError;
|
|
153
|
+
}
|
|
154
|
+
if (!operationCompleted) throw operationError;
|
|
155
|
+
return operationResult as TResult;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const withFileHandle = async <TResult>(
|
|
159
|
+
filePathArg: string,
|
|
160
|
+
flagsArg: 'r' | 'wx',
|
|
161
|
+
operationArg: (
|
|
162
|
+
fileHandleArg: Awaited<ReturnType<typeof fs.open>>,
|
|
163
|
+
) => Promise<TResult>,
|
|
164
|
+
signalArg?: AbortSignal,
|
|
165
|
+
): Promise<TResult> => runWithOwnedMigrationFileHandle(
|
|
166
|
+
await fs.open(filePathArg, flagsArg),
|
|
167
|
+
operationArg,
|
|
168
|
+
signalArg,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const writeBuffer = async (
|
|
172
|
+
fileHandleArg: Awaited<ReturnType<typeof fs.open>>,
|
|
173
|
+
bufferArg: Buffer,
|
|
174
|
+
signalArg?: AbortSignal,
|
|
175
|
+
): Promise<void> => {
|
|
176
|
+
let offset = 0;
|
|
177
|
+
while (offset < bufferArg.length) {
|
|
178
|
+
signalArg?.throwIfAborted();
|
|
179
|
+
const { bytesWritten } = await fileHandleArg.write(
|
|
180
|
+
bufferArg,
|
|
181
|
+
offset,
|
|
182
|
+
bufferArg.length - offset,
|
|
183
|
+
null,
|
|
184
|
+
);
|
|
185
|
+
if (bytesWritten <= 0) {
|
|
186
|
+
throw new Error('SmartDB storage migration write made no progress');
|
|
187
|
+
}
|
|
188
|
+
offset += bytesWritten;
|
|
189
|
+
}
|
|
190
|
+
signalArg?.throwIfAborted();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const syncDirectory = async (
|
|
194
|
+
directoryPathArg: string,
|
|
195
|
+
signalArg?: AbortSignal,
|
|
196
|
+
): Promise<void> => {
|
|
197
|
+
try {
|
|
198
|
+
await withFileHandle(directoryPathArg, 'r', async (directoryHandleArg) => {
|
|
199
|
+
await directoryHandleArg.sync();
|
|
200
|
+
}, signalArg);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (!['EINVAL', 'ENOTSUP'].includes((error as NodeJS.ErrnoException).code || '')) {
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const syncFile = async (filePathArg: string, signalArg?: AbortSignal): Promise<void> => {
|
|
209
|
+
await withFileHandle(filePathArg, 'r', async (fileHandleArg) => {
|
|
210
|
+
await fileHandleArg.sync();
|
|
211
|
+
}, signalArg);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const assertPublishedMigrationComplete = async (
|
|
215
|
+
collectionDirectoryArg: string,
|
|
216
|
+
): Promise<void> => {
|
|
217
|
+
for (const fileName of ['data.rdb', 'keydir.hint', 'indexes.json']) {
|
|
218
|
+
let fileStat: Awaited<ReturnType<typeof fs.lstat>>;
|
|
219
|
+
try {
|
|
220
|
+
fileStat = await fs.lstat(path.join(collectionDirectoryArg, fileName));
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`SmartDB legacy migration target is incomplete: ${collectionDirectoryArg}`,
|
|
225
|
+
{ cause: error },
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`SmartDB legacy migration target is incomplete: ${collectionDirectoryArg}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
103
238
|
// ---------------------------------------------------------------------------
|
|
104
239
|
// Migration: v0 (JSON) → v1 (Bitcask binary)
|
|
105
240
|
// ---------------------------------------------------------------------------
|
|
@@ -119,21 +254,27 @@ interface IKeyDirEntry {
|
|
|
119
254
|
* - Returns a list of old files that can be safely deleted.
|
|
120
255
|
* - On failure, cleans up any partial new files and throws.
|
|
121
256
|
*/
|
|
122
|
-
export async function migrateV0ToV1(
|
|
257
|
+
export async function migrateV0ToV1(
|
|
258
|
+
storagePath: string,
|
|
259
|
+
signalArg?: AbortSignal,
|
|
260
|
+
): Promise<string[]> {
|
|
123
261
|
const deletableFiles: string[] = [];
|
|
124
|
-
const
|
|
262
|
+
const stagingDirectories = new Set<string>();
|
|
125
263
|
|
|
126
264
|
try {
|
|
127
|
-
|
|
265
|
+
signalArg?.throwIfAborted();
|
|
266
|
+
const dbEntries = await fs.readdir(storagePath, { withFileTypes: true });
|
|
128
267
|
|
|
129
268
|
for (const dbEntry of dbEntries) {
|
|
269
|
+
signalArg?.throwIfAborted();
|
|
130
270
|
if (!dbEntry.isDirectory()) continue;
|
|
131
271
|
if (dbEntry.name.startsWith('.__rustdb_')) continue;
|
|
132
272
|
|
|
133
273
|
const dbDir = path.join(storagePath, dbEntry.name);
|
|
134
|
-
const collFiles = fs.
|
|
274
|
+
const collFiles = await fs.readdir(dbDir, { withFileTypes: true });
|
|
135
275
|
|
|
136
276
|
for (const collFile of collFiles) {
|
|
277
|
+
signalArg?.throwIfAborted();
|
|
137
278
|
if (!collFile.isFile()) continue;
|
|
138
279
|
if (collFile.name.startsWith('.__rustdb_')) continue;
|
|
139
280
|
if (!collFile.name.endsWith('.json')) continue;
|
|
@@ -145,35 +286,46 @@ export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
|
|
|
145
286
|
|
|
146
287
|
// Target directory
|
|
147
288
|
const collDir = path.join(dbDir, collName);
|
|
148
|
-
if (
|
|
149
|
-
|
|
289
|
+
if (await pathExists(collDir)) {
|
|
290
|
+
await assertPublishedMigrationComplete(collDir);
|
|
291
|
+
if (await pathExists(indexJsonPath)) deletableFiles.push(indexJsonPath);
|
|
292
|
+
deletableFiles.push(jsonPath);
|
|
150
293
|
continue;
|
|
151
294
|
}
|
|
152
295
|
|
|
153
296
|
console.log(`[smartdb] Migrating ${dbEntry.name}.${collName}...`);
|
|
154
297
|
|
|
155
298
|
// Read the JSON collection
|
|
156
|
-
const jsonData = fs.
|
|
299
|
+
const jsonData = await fs.readFile(jsonPath, {
|
|
300
|
+
encoding: 'utf8',
|
|
301
|
+
signal: signalArg,
|
|
302
|
+
});
|
|
303
|
+
signalArg?.throwIfAborted();
|
|
157
304
|
const docs: any[] = JSON.parse(jsonData);
|
|
158
305
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
306
|
+
// Build the complete collection in a hidden sibling and publish it
|
|
307
|
+
// atomically so cancellation can never expose partial v1 state.
|
|
308
|
+
const stagingDir = path.join(
|
|
309
|
+
dbDir,
|
|
310
|
+
`.__rustdb_migration_${collName}_${process.pid}_${crypto.randomBytes(8).toString('hex')}`,
|
|
311
|
+
);
|
|
312
|
+
await fs.mkdir(stagingDir);
|
|
313
|
+
stagingDirectories.add(stagingDir);
|
|
314
|
+
signalArg?.throwIfAborted();
|
|
162
315
|
|
|
163
316
|
// Write data.rdb
|
|
164
|
-
const dataPath = path.join(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
try {
|
|
317
|
+
const dataPath = path.join(stagingDir, 'data.rdb');
|
|
318
|
+
await withFileHandle(dataPath, 'wx', async (dataFileHandle) => {
|
|
168
319
|
// File header
|
|
169
320
|
const headerBuf = writeFileHeader(FILE_TYPE_DATA);
|
|
170
|
-
|
|
321
|
+
await writeBuffer(dataFileHandle, headerBuf, signalArg);
|
|
171
322
|
|
|
172
323
|
let currentOffset = BigInt(FILE_HEADER_SIZE);
|
|
173
324
|
const keydir: Map<string, IKeyDirEntry> = new Map();
|
|
174
325
|
const ts = BigInt(Date.now());
|
|
175
326
|
|
|
176
327
|
for (const doc of docs) {
|
|
328
|
+
signalArg?.throwIfAborted();
|
|
177
329
|
// Extract _id
|
|
178
330
|
let idHex: string;
|
|
179
331
|
if (doc._id && doc._id.$oid) {
|
|
@@ -194,7 +346,7 @@ export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
|
|
|
194
346
|
const valueBuf = Buffer.from(bsonBytes);
|
|
195
347
|
|
|
196
348
|
const record = encodeDataRecord(ts, keyBuf, valueBuf);
|
|
197
|
-
|
|
349
|
+
await writeBuffer(dataFileHandle, record, signalArg);
|
|
198
350
|
|
|
199
351
|
keydir.set(idHex, {
|
|
200
352
|
offset: currentOffset,
|
|
@@ -206,47 +358,75 @@ export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
|
|
|
206
358
|
currentOffset += BigInt(record.length);
|
|
207
359
|
}
|
|
208
360
|
|
|
209
|
-
|
|
210
|
-
|
|
361
|
+
await dataFileHandle.sync();
|
|
362
|
+
signalArg?.throwIfAborted();
|
|
211
363
|
|
|
212
364
|
// Write keydir.hint
|
|
213
|
-
const hintPath = path.join(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
365
|
+
const hintPath = path.join(stagingDir, 'keydir.hint');
|
|
366
|
+
await withFileHandle(hintPath, 'wx', async (hintFileHandle) => {
|
|
367
|
+
await writeBuffer(hintFileHandle, writeFileHeader(FILE_TYPE_HINT), signalArg);
|
|
368
|
+
for (const [key, entry] of keydir) {
|
|
369
|
+
signalArg?.throwIfAborted();
|
|
370
|
+
await writeBuffer(
|
|
371
|
+
hintFileHandle,
|
|
372
|
+
encodeHintEntry(
|
|
373
|
+
key,
|
|
374
|
+
entry.offset,
|
|
375
|
+
entry.recordLen,
|
|
376
|
+
entry.valueLen,
|
|
377
|
+
entry.timestamp,
|
|
378
|
+
),
|
|
379
|
+
signalArg,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
await hintFileHandle.sync();
|
|
383
|
+
signalArg?.throwIfAborted();
|
|
384
|
+
}, signalArg);
|
|
385
|
+
}, signalArg);
|
|
227
386
|
|
|
228
387
|
// Copy indexes.json if it exists
|
|
229
|
-
if (
|
|
230
|
-
const destIndexPath = path.join(
|
|
231
|
-
fs.
|
|
388
|
+
if (await pathExists(indexJsonPath)) {
|
|
389
|
+
const destIndexPath = path.join(stagingDir, 'indexes.json');
|
|
390
|
+
await fs.copyFile(indexJsonPath, destIndexPath);
|
|
391
|
+
await syncFile(destIndexPath, signalArg);
|
|
392
|
+
signalArg?.throwIfAborted();
|
|
232
393
|
deletableFiles.push(indexJsonPath);
|
|
233
394
|
} else {
|
|
234
395
|
// Write default _id index
|
|
235
|
-
const destIndexPath = path.join(
|
|
236
|
-
fs.
|
|
396
|
+
const destIndexPath = path.join(stagingDir, 'indexes.json');
|
|
397
|
+
await fs.writeFile(
|
|
398
|
+
destIndexPath,
|
|
399
|
+
JSON.stringify([{ name: '_id_', key: { _id: 1 } }], null, 2),
|
|
400
|
+
{ signal: signalArg },
|
|
401
|
+
);
|
|
402
|
+
await syncFile(destIndexPath, signalArg);
|
|
403
|
+
signalArg?.throwIfAborted();
|
|
237
404
|
}
|
|
238
405
|
|
|
406
|
+
await assertPublishedMigrationComplete(stagingDir);
|
|
407
|
+
await syncDirectory(stagingDir, signalArg);
|
|
408
|
+
signalArg?.throwIfAborted();
|
|
409
|
+
await fs.rename(stagingDir, collDir);
|
|
410
|
+
stagingDirectories.delete(stagingDir);
|
|
411
|
+
await syncDirectory(dbDir, signalArg);
|
|
412
|
+
signalArg?.throwIfAborted();
|
|
239
413
|
deletableFiles.push(jsonPath);
|
|
240
414
|
|
|
241
415
|
console.log(`[smartdb] Migrated ${dbEntry.name}.${collName}: ${docs.length} documents`);
|
|
242
416
|
}
|
|
243
417
|
}
|
|
244
418
|
} catch (err) {
|
|
245
|
-
|
|
246
|
-
for (const dir of
|
|
419
|
+
const cleanupErrors: unknown[] = [];
|
|
420
|
+
for (const dir of Array.from(stagingDirectories).reverse()) {
|
|
247
421
|
try {
|
|
248
|
-
fs.
|
|
249
|
-
|
|
422
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
423
|
+
await syncDirectory(path.dirname(dir));
|
|
424
|
+
} catch (cleanupError) {
|
|
425
|
+
cleanupErrors.push(cleanupError);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (cleanupErrors.length > 0) {
|
|
429
|
+
throw new SmartDbStorageMigrationCleanupError(err, cleanupErrors);
|
|
250
430
|
}
|
|
251
431
|
throw err;
|
|
252
432
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TAuthMetadataPermissionTransitionStatus } from '../../ts_smartdb/rust-db-bridge.js';
|
|
2
|
+
import type { ISmartDbManagementOperationOptions } from '../../ts_smartdb/service-types.js';
|
|
2
3
|
|
|
3
4
|
const legacyAuthMetadataMode = 0o644;
|
|
4
5
|
const privateAuthMetadataMode = 0o600;
|
|
@@ -8,6 +9,7 @@ export interface IAuthMetadataPermissionMigrationBridge {
|
|
|
8
9
|
usersPathArg: string,
|
|
9
10
|
expectedModeArg: number,
|
|
10
11
|
targetModeArg: number,
|
|
12
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
11
13
|
): Promise<TAuthMetadataPermissionTransitionStatus>;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -22,9 +24,11 @@ export type TAuthMetadataPermissionMigrationStatus =
|
|
|
22
24
|
export const migrateV1AuthMetadataPermissions = async (
|
|
23
25
|
bridgeArg: IAuthMetadataPermissionMigrationBridge,
|
|
24
26
|
usersPathArg: string,
|
|
27
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
25
28
|
): Promise<TAuthMetadataPermissionMigrationStatus> => bridgeArg
|
|
26
29
|
.transitionAuthMetadataPermissions(
|
|
27
30
|
usersPathArg,
|
|
28
31
|
legacyAuthMetadataMode,
|
|
29
32
|
privateAuthMetadataMode,
|
|
33
|
+
optionsArg,
|
|
30
34
|
);
|
|
@@ -537,20 +537,41 @@ export class RustDbBridge extends EventEmitter {
|
|
|
537
537
|
usersPathArg: string,
|
|
538
538
|
expectedModeArg: number,
|
|
539
539
|
targetModeArg: number,
|
|
540
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
540
541
|
): Promise<TAuthMetadataPermissionTransitionStatus> {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
542
|
+
validateSmartDbManagementOperationOptions(optionsArg);
|
|
543
|
+
try {
|
|
544
|
+
const result = await this.bridge.sendCommand(
|
|
545
|
+
'transitionAuthMetadataPermissions',
|
|
546
|
+
{
|
|
547
|
+
usersPath: usersPathArg,
|
|
548
|
+
expectedMode: expectedModeArg,
|
|
549
|
+
targetMode: targetModeArg,
|
|
550
|
+
},
|
|
551
|
+
optionsArg,
|
|
552
|
+
) as { status: TAuthMetadataPermissionTransitionStatus };
|
|
553
|
+
return result.status;
|
|
554
|
+
} catch (error) {
|
|
555
|
+
await this.awaitManagementOperationTermination(error);
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
550
558
|
}
|
|
551
559
|
|
|
552
|
-
public async startDb(
|
|
553
|
-
|
|
560
|
+
public async startDb(
|
|
561
|
+
config: ISmartDbRustConfig,
|
|
562
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
563
|
+
): Promise<{ connectionUri: string }> {
|
|
564
|
+
validateSmartDbManagementOperationOptions(optionsArg);
|
|
565
|
+
try {
|
|
566
|
+
return await this.bridge.sendCommand(
|
|
567
|
+
'start',
|
|
568
|
+
{ config },
|
|
569
|
+
optionsArg,
|
|
570
|
+
) as { connectionUri: string };
|
|
571
|
+
} catch (error) {
|
|
572
|
+
await this.awaitManagementOperationTermination(error);
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
554
575
|
}
|
|
555
576
|
|
|
556
577
|
public async stopDb(): Promise<void> {
|
|
@@ -2,7 +2,12 @@ import {
|
|
|
2
2
|
RustDbBridge,
|
|
3
3
|
SmartDbManagementOperationTerminationError,
|
|
4
4
|
} from '../rust-db-bridge.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AuthMetadataMigrationRunner,
|
|
7
|
+
SmartDbStorageMigrationCleanupError,
|
|
8
|
+
StorageMigrator,
|
|
9
|
+
} from '../../ts_migration/index.js';
|
|
10
|
+
import { validateSmartDbManagementOperationOptions } from '../offline-inspection.js';
|
|
6
11
|
import type {
|
|
7
12
|
IOpLogEntry,
|
|
8
13
|
IOpLogResult,
|
|
@@ -148,11 +153,63 @@ export class SmartdbServer {
|
|
|
148
153
|
/**
|
|
149
154
|
* Start the server
|
|
150
155
|
*/
|
|
151
|
-
async start(): Promise<void> {
|
|
156
|
+
async start(optionsArg?: ISmartDbManagementOperationOptions): Promise<void> {
|
|
157
|
+
validateSmartDbManagementOperationOptions(optionsArg);
|
|
158
|
+
optionsArg?.signal?.throwIfAborted();
|
|
152
159
|
if (this.isRunning || this.startInProgress || this.bridgeOwned) {
|
|
153
160
|
throw new Error('Server is already running or still owns a Rust bridge');
|
|
154
161
|
}
|
|
155
162
|
|
|
163
|
+
const startupStartedAt = Date.now();
|
|
164
|
+
const startupDeadline = optionsArg?.timeoutMs === undefined
|
|
165
|
+
? undefined
|
|
166
|
+
: startupStartedAt + optionsArg.timeoutMs;
|
|
167
|
+
const timeoutError = new DOMException(
|
|
168
|
+
optionsArg?.timeoutMs === undefined
|
|
169
|
+
? 'SmartDB startup timed out'
|
|
170
|
+
: `SmartDB startup timed out after ${optionsArg.timeoutMs}ms`,
|
|
171
|
+
'TimeoutError',
|
|
172
|
+
);
|
|
173
|
+
const startupAbortController = new AbortController();
|
|
174
|
+
let startupTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
175
|
+
let externalAbortHandler: (() => void) | undefined;
|
|
176
|
+
if (optionsArg?.signal) {
|
|
177
|
+
externalAbortHandler = () => {
|
|
178
|
+
startupAbortController.abort(optionsArg.signal!.reason);
|
|
179
|
+
};
|
|
180
|
+
optionsArg.signal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
181
|
+
}
|
|
182
|
+
if (optionsArg?.timeoutMs !== undefined) {
|
|
183
|
+
startupTimeout = setTimeout(() => {
|
|
184
|
+
startupAbortController.abort(timeoutError);
|
|
185
|
+
}, optionsArg.timeoutMs);
|
|
186
|
+
}
|
|
187
|
+
const checkpoint = () => {
|
|
188
|
+
if (
|
|
189
|
+
startupDeadline !== undefined
|
|
190
|
+
&& Date.now() >= startupDeadline
|
|
191
|
+
&& !startupAbortController.signal.aborted
|
|
192
|
+
) {
|
|
193
|
+
startupAbortController.abort(timeoutError);
|
|
194
|
+
}
|
|
195
|
+
startupAbortController.signal.throwIfAborted();
|
|
196
|
+
};
|
|
197
|
+
const getBridgeOperationOptions = (): ISmartDbManagementOperationOptions | undefined => {
|
|
198
|
+
checkpoint();
|
|
199
|
+
const remainingTimeoutMs = startupDeadline === undefined
|
|
200
|
+
? undefined
|
|
201
|
+
: startupDeadline - Date.now();
|
|
202
|
+
if (remainingTimeoutMs !== undefined && remainingTimeoutMs <= 0) {
|
|
203
|
+
startupAbortController.abort(timeoutError);
|
|
204
|
+
checkpoint();
|
|
205
|
+
}
|
|
206
|
+
if (!optionsArg?.signal && remainingTimeoutMs === undefined) return undefined;
|
|
207
|
+
return {
|
|
208
|
+
...(optionsArg?.signal ? { signal: optionsArg.signal } : {}),
|
|
209
|
+
...(remainingTimeoutMs === undefined ? {} : { timeoutMs: remainingTimeoutMs }),
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
|
|
156
213
|
this.startInProgress = true;
|
|
157
214
|
let resolveStartCompletion!: () => void;
|
|
158
215
|
const startCompletionPromise = new Promise<void>((resolveArg) => {
|
|
@@ -160,16 +217,19 @@ export class SmartdbServer {
|
|
|
160
217
|
});
|
|
161
218
|
this.startCompletionPromise = startCompletionPromise;
|
|
162
219
|
try {
|
|
220
|
+
checkpoint();
|
|
163
221
|
// Run storage migration for file-based storage before starting Rust engine
|
|
164
222
|
if (this.options.storage === 'file' && this.options.storagePath) {
|
|
165
223
|
const migrator = new StorageMigrator(this.options.storagePath);
|
|
166
|
-
await migrator.run();
|
|
224
|
+
await migrator.run(startupAbortController.signal);
|
|
225
|
+
checkpoint();
|
|
167
226
|
}
|
|
168
227
|
|
|
169
228
|
// Ownership starts before spawn because a rejected spawn can still have
|
|
170
229
|
// created a child whose termination must be confirmed.
|
|
171
230
|
this.bridgeOwned = true;
|
|
172
|
-
const spawned = await this.bridge.spawn();
|
|
231
|
+
const spawned = await this.bridge.spawn(getBridgeOperationOptions());
|
|
232
|
+
checkpoint();
|
|
173
233
|
if (!spawned) {
|
|
174
234
|
throw new Error(
|
|
175
235
|
'smartdb Rust binary not found. Set SMARTDB_RUST_BINARY env var, ' +
|
|
@@ -181,7 +241,11 @@ export class SmartdbServer {
|
|
|
181
241
|
const authMetadataMigrationRunner = new AuthMetadataMigrationRunner(
|
|
182
242
|
this.options.auth.usersPath,
|
|
183
243
|
);
|
|
184
|
-
await authMetadataMigrationRunner.run(
|
|
244
|
+
await authMetadataMigrationRunner.run(
|
|
245
|
+
this.bridge,
|
|
246
|
+
getBridgeOperationOptions(),
|
|
247
|
+
);
|
|
248
|
+
checkpoint();
|
|
185
249
|
}
|
|
186
250
|
|
|
187
251
|
// Send config, get back connectionUri
|
|
@@ -196,7 +260,8 @@ export class SmartdbServer {
|
|
|
196
260
|
auth: this.options.auth,
|
|
197
261
|
tls: this.options.tls,
|
|
198
262
|
oplog: this.options.oplog,
|
|
199
|
-
});
|
|
263
|
+
}, getBridgeOperationOptions());
|
|
264
|
+
checkpoint();
|
|
200
265
|
if (!this.bridgeOwned || !this.bridge.running) {
|
|
201
266
|
throw new Error('smartdb Rust process exited during startup');
|
|
202
267
|
}
|
|
@@ -216,8 +281,19 @@ export class SmartdbServer {
|
|
|
216
281
|
this.resolvedPort = resolvedPort;
|
|
217
282
|
}
|
|
218
283
|
this.resolvedConnectionUri = result.connectionUri;
|
|
284
|
+
checkpoint();
|
|
219
285
|
this.isRunning = true;
|
|
220
286
|
} catch (error) {
|
|
287
|
+
const startupError = error instanceof SmartDbManagementOperationTerminationError
|
|
288
|
+
|| error instanceof SmartDbStorageMigrationCleanupError
|
|
289
|
+
? error
|
|
290
|
+
: optionsArg?.signal?.aborted
|
|
291
|
+
? optionsArg.signal.reason
|
|
292
|
+
: startupAbortController.signal.aborted || (
|
|
293
|
+
startupDeadline !== undefined && Date.now() >= startupDeadline
|
|
294
|
+
)
|
|
295
|
+
? timeoutError
|
|
296
|
+
: error;
|
|
221
297
|
this.isRunning = false;
|
|
222
298
|
this.resolvedConnectionUri = '';
|
|
223
299
|
this.resolvedPort = undefined;
|
|
@@ -227,14 +303,18 @@ export class SmartdbServer {
|
|
|
227
303
|
this.bridgeOwned = false;
|
|
228
304
|
} catch (cleanupError) {
|
|
229
305
|
throw new AggregateError(
|
|
230
|
-
[
|
|
306
|
+
[startupError, cleanupError],
|
|
231
307
|
'SmartDB startup failed and Rust bridge cleanup was incomplete',
|
|
232
|
-
{ cause:
|
|
308
|
+
{ cause: startupError },
|
|
233
309
|
);
|
|
234
310
|
}
|
|
235
311
|
}
|
|
236
|
-
throw
|
|
312
|
+
throw startupError;
|
|
237
313
|
} finally {
|
|
314
|
+
if (startupTimeout) clearTimeout(startupTimeout);
|
|
315
|
+
if (externalAbortHandler && optionsArg?.signal) {
|
|
316
|
+
optionsArg.signal.removeEventListener('abort', externalAbortHandler);
|
|
317
|
+
}
|
|
238
318
|
this.startInProgress = false;
|
|
239
319
|
resolveStartCompletion();
|
|
240
320
|
if (this.startCompletionPromise === startCompletionPromise) {
|