@lossless.org/nosqldb 10.11.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.smartconfig.json +117 -0
  2. package/dist_rust/rustdb_linux_amd64 +0 -0
  3. package/dist_rust/rustdb_linux_amd64.tsrust-build.json +5 -5
  4. package/dist_rust/rustdb_linux_amd64_musl +0 -0
  5. package/dist_rust/rustdb_linux_amd64_musl.tsrust-build.json +5 -5
  6. package/dist_rust/rustdb_linux_arm64 +0 -0
  7. package/dist_rust/rustdb_linux_arm64.tsrust-build.json +5 -5
  8. package/dist_rust/rustdb_linux_arm64_musl +0 -0
  9. package/dist_rust/rustdb_linux_arm64_musl.tsrust-build.json +5 -5
  10. package/dist_rust/rustdb_macos_amd64 +0 -0
  11. package/dist_rust/rustdb_macos_amd64.tsrust-build.json +5 -5
  12. package/dist_rust/rustdb_macos_arm64 +0 -0
  13. package/dist_rust/rustdb_macos_arm64.tsrust-build.json +5 -5
  14. package/dist_ts/00_commitinfo_data.js +2 -2
  15. package/dist_ts/ts_local/classes.localnosqldb.d.ts +15 -4
  16. package/dist_ts/ts_local/classes.localnosqldb.js +31 -11
  17. package/dist_ts/ts_nosqldb/index.d.ts +3 -3
  18. package/dist_ts/ts_nosqldb/index.js +5 -4
  19. package/dist_ts/ts_nosqldb/rust-db-bridge.d.ts +125 -2
  20. package/dist_ts/ts_nosqldb/rust-db-bridge.js +261 -46
  21. package/dist_ts/ts_nosqldb/server/classes.nosqldbserver.d.ts +69 -3
  22. package/dist_ts/ts_nosqldb/server/classes.nosqldbserver.js +91 -12
  23. package/dist_ts/ts_nosqldb/server/index.d.ts +2 -2
  24. package/dist_ts/ts_nosqldb/server/index.js +2 -2
  25. package/dist_ts/ts_nosqldb/stopped-storage.d.ts +9 -1
  26. package/dist_ts/ts_nosqldb/stopped-storage.js +1 -1
  27. package/dist_ts/ts_nosqldb/storage-rehearsal.js +1 -1
  28. package/dist_ts/ts_nosqldb/storage-root-alignment.js +7 -3
  29. package/dist_ts/ts_nosqldb/storage-root-relocation.js +2 -2
  30. package/dist_ts_debugserver/bundled.js +6 -2
  31. package/package.json +28 -15
  32. package/readme.md +470 -730
  33. package/third-party-notices.md +40 -8
  34. package/third_party_licenses/inventory.json +6 -6
  35. package/ts/00_commitinfo_data.ts +8 -0
  36. package/ts/index.ts +85 -0
  37. package/ts/ts_local/classes.localnosqldb.ts +631 -0
  38. package/ts/ts_local/index.ts +19 -0
  39. package/ts/ts_local/plugins.ts +1 -0
  40. package/ts/ts_nosqldb/classes.storagerehearsal.ts +109 -0
  41. package/ts/ts_nosqldb/committed-writes.ts +38 -0
  42. package/ts/ts_nosqldb/file-storage-options.ts +33 -0
  43. package/ts/ts_nosqldb/file-storage-startup.ts +10 -0
  44. package/ts/ts_nosqldb/index.ts +132 -0
  45. package/ts/ts_nosqldb/offline-inspection.ts +375 -0
  46. package/ts/ts_nosqldb/plugins.ts +12 -0
  47. package/ts/ts_nosqldb/resource-fencing.ts +368 -0
  48. package/ts/ts_nosqldb/rust-db-bridge.ts +1309 -0
  49. package/ts/ts_nosqldb/server/classes.nosqldbserver.ts +874 -0
  50. package/ts/ts_nosqldb/server/index.ts +4 -0
  51. package/ts/ts_nosqldb/service-types.ts +499 -0
  52. package/ts/ts_nosqldb/stopped-storage.ts +116 -0
  53. package/ts/ts_nosqldb/storage-rehearsal.ts +215 -0
  54. package/ts/ts_nosqldb/storage-root-alignment.ts +180 -0
  55. package/ts/ts_nosqldb/storage-root-relocation.ts +279 -0
@@ -0,0 +1,631 @@
1
+ import {
2
+ normalizeStoppedStorageRootAlignmentInput, normalizeStoppedStorageRootAlignmentResult,
3
+ normalizeStoppedStorageRootAlignmentAbortResult,
4
+ type ILocalNoSqlDbStoppedStorageRootAlignmentInput, type TLocalNoSqlDbStoppedStorageRootAlignmentResult,
5
+ type ILocalNoSqlDbStoppedStorageRootAlignmentAbortResult,
6
+ } from '../ts_nosqldb/storage-root-alignment.js';
7
+ import {
8
+ NoSqlDbStoppedOperationError,
9
+ normalizeStoppedStorageRootInspectionInput,
10
+ validateNoSqlDbStoppedOperationOptions,
11
+ isStoppedOperationUnknownMethod,
12
+ type INoSqlDbStoppedOperationOptions,
13
+ type ILocalNoSqlDbStoppedStorageRootInspectionInput,
14
+ type TLocalNoSqlDbStoppedStorageRootInspection,
15
+ } from '../ts_nosqldb/stopped-storage.js';
16
+ import * as crypto from 'node:crypto';
17
+ import * as fs from 'node:fs/promises';
18
+ import * as net from 'node:net';
19
+ import * as path from 'node:path';
20
+ import * as os from 'node:os';
21
+ import {
22
+ NoSqlDbServer,
23
+ type NoSqlDbEngineExitError,
24
+ type INoSqlDbAuthOptions,
25
+ type ILocalNoSqlDbOfflinePhysicalNamespaceInspection,
26
+ type ILocalNoSqlDbOfflinePhysicalNamespaceInspectionResult,
27
+ type ILocalNoSqlDbStoppedStorageRootRelocationInput,
28
+ type ILocalNoSqlDbStoppedStorageRootRelocationReceipt,
29
+ type ILocalNoSqlDbOfflineStringValueInspection,
30
+ type INoSqlDbManagementOperationOptions,
31
+ type TLocalNoSqlDbOfflineStringValueInspectionResult,
32
+ } from '../ts_nosqldb/index.js';
33
+ import {
34
+ RustDbBridge,
35
+ attachNoSqlDbEngineStderrTail,
36
+ rustDbBridgeStorageRootRelocation,
37
+ rustDbBridgeStorageRootAlignment,
38
+ rustDbBridgeStorageRootAlignmentAbort,
39
+ } from '../ts_nosqldb/rust-db-bridge.js';
40
+ import { validateNoSqlDbStorageRehearsalStartup } from '../ts_nosqldb/storage-rehearsal.js';
41
+ import { noStopRequestResult, type INoSqlDbServerStopResult } from '../ts_nosqldb/server/classes.nosqldbserver.js';
42
+ import {
43
+ type TNoSqlDbFileStorageStartup,
44
+ } from '../ts_nosqldb/file-storage-startup.js';
45
+ import {
46
+ normalizeNoSqlDbFileStorageOptions,
47
+ } from '../ts_nosqldb/file-storage-options.js';
48
+ import {
49
+ normalizeLocalNoSqlDbOfflinePhysicalNamespaceInspectionInput,
50
+ normalizeLocalNoSqlDbOfflineInspectionInput,
51
+ validateNoSqlDbManagementOperationOptions,
52
+ } from '../ts_nosqldb/offline-inspection.js';
53
+ import {
54
+ assertNoLocalNoSqlDbStorageRootRelocationWorkspace,
55
+ isLocalNoSqlDbStorageRootRelocationErrorCode,
56
+ LocalNoSqlDbStorageRootRelocationError,
57
+ normalizeLocalNoSqlDbStoppedStorageRootRelocationInput,
58
+ normalizeLocalNoSqlDbStoppedStorageRootRelocationReceipt,
59
+ } from '../ts_nosqldb/storage-root-relocation.js';
60
+
61
+ /**
62
+ * Connection information returned by LocalNoSqlDb.start()
63
+ */
64
+ export interface ILocalNoSqlDbConnectionInfo {
65
+ /** The Unix socket file path */
66
+ socketPath: string;
67
+ /** Connection URI (mongodb:// scheme) ready for MongoClient */
68
+ connectionUri: string;
69
+ }
70
+
71
+ export interface ILocalNoSqlDbOptions {
72
+ /** Persisted authentication, including the source-verbatim credentials of a rehearsal destination. */
73
+ auth?: INoSqlDbAuthOptions;
74
+ /** Exact rehearsal operation ID; requires current-format-only startup and an explicit private socket. */
75
+ storageRehearsalId?: string;
76
+ /** Exact engine executable. Invalid explicit paths reject without chmod or discovery fallback. Omit for normal discovery. */
77
+ binaryPath?: string;
78
+ /** Default: current-format-only. existing-only refuses missing, empty or uninitialized storage. */
79
+ fileStorageStartup?: TNoSqlDbFileStorageStartup;
80
+ /** Required: where to store data */
81
+ folderPath: string;
82
+ /** Optional: custom socket path (default: auto-generated in /tmp) */
83
+ socketPath?: string;
84
+ /** Receives every engine standard-error line; as `INoSqlDbServerOptions.engineLog`. */
85
+ engineLog?: (line: string) => void;
86
+ /** Called once for each unexpected engine exit; as `INoSqlDbServerOptions.onEngineExit`. */
87
+ onEngineExit?: (error: NoSqlDbEngineExitError) => void;
88
+ }
89
+
90
+ /**
91
+ * LocalNoSqlDb - Lightweight local MongoDB-compatible database using Unix sockets
92
+ *
93
+ * This class wraps NoSqlDbServer and provides a simple interface for
94
+ * starting a local file-based database server. Returns connection
95
+ * info that you can use with any compatible driver instance.
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * import { LocalNoSqlDb } from '@lossless.org/nosqldb';
100
+ * import { MongoClient } from 'mongodb';
101
+ *
102
+ * const db = new LocalNoSqlDb({ folderPath: './data' });
103
+ * const { connectionUri } = await db.start();
104
+ *
105
+ * // Connect with the driver
106
+ * const client = new MongoClient(connectionUri, { directConnection: true });
107
+ * await client.connect();
108
+ *
109
+ * // Use the client
110
+ * const collection = client.db('mydb').collection('users');
111
+ * await collection.insertOne({ name: 'Alice' });
112
+ *
113
+ * // When done
114
+ * await client.close();
115
+ * await db.stop();
116
+ * ```
117
+ */
118
+ export class LocalNoSqlDb {
119
+ private static storageRootRelocationGate: Promise<void> = Promise.resolve();
120
+ private static storageRootRelocationBridge: RustDbBridge | undefined;
121
+ private options: ILocalNoSqlDbOptions;
122
+ private server: NoSqlDbServer | null = null;
123
+ private generatedSocketPath: string | null = null;
124
+
125
+ constructor(options: ILocalNoSqlDbOptions) {
126
+ validateNoSqlDbStorageRehearsalStartup({ ...options, storage: 'file', storagePath: options.folderPath });
127
+ this.options = {
128
+ ...options,
129
+ ...normalizeNoSqlDbFileStorageOptions({ ...options, storage: 'file' }),
130
+ };
131
+ }
132
+
133
+ private static createOfflineInspectionBridge(binaryPath?: string): RustDbBridge {
134
+ return new RustDbBridge({ binaryPath });
135
+ }
136
+
137
+ private static createStorageRootRelocationBridge(binaryPath?: string): RustDbBridge {
138
+ return new RustDbBridge({ binaryPath });
139
+ }
140
+
141
+ private static async withStorageRootRelocationGate<TResult>(
142
+ operationArg: () => Promise<TResult>,
143
+ ): Promise<TResult> {
144
+ const previous = LocalNoSqlDb.storageRootRelocationGate;
145
+ let release!: () => void;
146
+ LocalNoSqlDb.storageRootRelocationGate = new Promise<void>((resolveArg) => {
147
+ release = resolveArg;
148
+ });
149
+ await previous;
150
+ try {
151
+ return await operationArg();
152
+ } finally {
153
+ release();
154
+ }
155
+ }
156
+
157
+ private static remainingManagementTimeout(
158
+ deadlineArg: number | undefined,
159
+ ): number | undefined {
160
+ if (deadlineArg === undefined) return undefined;
161
+ const remaining = deadlineArg - Date.now();
162
+ if (remaining <= 0) {
163
+ throw new LocalNoSqlDbStorageRootRelocationError('IO');
164
+ }
165
+ return remaining;
166
+ }
167
+
168
+ /**
169
+ * Relocate one stopped current native file root, including authenticated
170
+ * roots, under continuous ownership. Preserve the returned source receipt;
171
+ * interrupted operations require the identical request to finish recovery.
172
+ */
173
+ public static async relocateStoppedStorageRoot(
174
+ inputArg: ILocalNoSqlDbStoppedStorageRootRelocationInput,
175
+ optionsArg?: INoSqlDbStoppedOperationOptions,
176
+ ): Promise<ILocalNoSqlDbStoppedStorageRootRelocationReceipt> {
177
+ let input: ILocalNoSqlDbStoppedStorageRootRelocationInput;
178
+ try {
179
+ input = normalizeLocalNoSqlDbStoppedStorageRootRelocationInput(inputArg);
180
+ validateNoSqlDbStoppedOperationOptions(optionsArg);
181
+ } catch (error) {
182
+ if (error instanceof LocalNoSqlDbStorageRootRelocationError) throw error;
183
+ throw new LocalNoSqlDbStorageRootRelocationError('INVALID_REQUEST');
184
+ }
185
+ if (optionsArg?.signal?.aborted) {
186
+ throw new LocalNoSqlDbStorageRootRelocationError('IO');
187
+ }
188
+ if (process.platform !== 'linux' && process.platform !== 'darwin') {
189
+ throw new LocalNoSqlDbStorageRootRelocationError('UNSUPPORTED');
190
+ }
191
+ const deadline = optionsArg?.timeoutMs === undefined
192
+ ? undefined
193
+ : Date.now() + optionsArg.timeoutMs;
194
+
195
+ return LocalNoSqlDb.withStorageRootRelocationGate(async () => {
196
+ const retainedBridge = LocalNoSqlDb.storageRootRelocationBridge;
197
+ if (retainedBridge) {
198
+ try {
199
+ await retainedBridge.terminate({ gracePeriodMs: 0 });
200
+ } catch {
201
+ throw new LocalNoSqlDbStorageRootRelocationError('RECOVERY_REQUIRED');
202
+ }
203
+ if (!retainedBridge.ownershipReleased) {
204
+ throw new LocalNoSqlDbStorageRootRelocationError('RECOVERY_REQUIRED');
205
+ }
206
+ if (LocalNoSqlDb.storageRootRelocationBridge === retainedBridge) {
207
+ LocalNoSqlDb.storageRootRelocationBridge = undefined;
208
+ }
209
+ }
210
+
211
+ let bridge: RustDbBridge | undefined;
212
+ let receipt: ILocalNoSqlDbStoppedStorageRootRelocationReceipt | undefined;
213
+ let operationError: LocalNoSqlDbStorageRootRelocationError | undefined;
214
+ try {
215
+ bridge = LocalNoSqlDb.createStorageRootRelocationBridge(optionsArg?.binaryPath);
216
+ LocalNoSqlDb.storageRootRelocationBridge = bridge;
217
+ const spawnTimeout = LocalNoSqlDb.remainingManagementTimeout(deadline);
218
+ const spawned = await bridge.spawn({
219
+ ...(optionsArg?.signal ? { signal: optionsArg.signal } : {}),
220
+ ...(spawnTimeout === undefined ? {} : { timeoutMs: spawnTimeout }),
221
+ });
222
+ if (!spawned) {
223
+ throw new LocalNoSqlDbStorageRootRelocationError('IO');
224
+ }
225
+ const commandTimeout = LocalNoSqlDb.remainingManagementTimeout(deadline);
226
+ const result = await bridge[rustDbBridgeStorageRootRelocation](input, {
227
+ ...(optionsArg?.signal ? { signal: optionsArg.signal } : {}),
228
+ ...(commandTimeout === undefined ? {} : { timeoutMs: commandTimeout }),
229
+ });
230
+ receipt = normalizeLocalNoSqlDbStoppedStorageRootRelocationReceipt(result, input);
231
+ } catch (error) {
232
+ if (error instanceof NoSqlDbStoppedOperationError || isStoppedOperationUnknownMethod(error, 'relocateStoppedStorageRoot')) {
233
+ operationError = new LocalNoSqlDbStorageRootRelocationError(error instanceof NoSqlDbStoppedOperationError ? error.code : 'UNSUPPORTED');
234
+ } else if (error instanceof LocalNoSqlDbStorageRootRelocationError) {
235
+ operationError = error;
236
+ } else {
237
+ const requestWasDispatched = error instanceof Error
238
+ && 'requestId' in error
239
+ && typeof error.requestId === 'string';
240
+ if (
241
+ requestWasDispatched
242
+ && error instanceof Error
243
+ && 'responseErrorCode' in error
244
+ && isLocalNoSqlDbStorageRootRelocationErrorCode(error.responseErrorCode)
245
+ ) {
246
+ operationError = new LocalNoSqlDbStorageRootRelocationError(error.responseErrorCode);
247
+ } else {
248
+ operationError = new LocalNoSqlDbStorageRootRelocationError(
249
+ requestWasDispatched ? 'RECOVERY_REQUIRED' : 'IO',
250
+ );
251
+ }
252
+ }
253
+ }
254
+
255
+ if (bridge) {
256
+ try {
257
+ await bridge.terminate({ gracePeriodMs: 0 });
258
+ if (!bridge.ownershipReleased) {
259
+ throw new Error('termination was not confirmed');
260
+ }
261
+ if (LocalNoSqlDb.storageRootRelocationBridge === bridge) {
262
+ LocalNoSqlDb.storageRootRelocationBridge = undefined;
263
+ }
264
+ } catch {
265
+ operationError = new LocalNoSqlDbStorageRootRelocationError('RECOVERY_REQUIRED');
266
+ }
267
+ }
268
+ if (operationError) throw operationError;
269
+ if (!receipt) {
270
+ throw new LocalNoSqlDbStorageRootRelocationError('RECOVERY_REQUIRED');
271
+ }
272
+ return receipt;
273
+ });
274
+ }
275
+
276
+ private static createStorageRootAlignmentBridge(binaryPath?: string): RustDbBridge {
277
+ return new RustDbBridge({ binaryPath });
278
+ }
279
+
280
+ /** Adopt an authority-free stopped copy as an independent provider. Retain its external receipt. */
281
+ public static alignStoppedStorageRoot(input: ILocalNoSqlDbStoppedStorageRootAlignmentInput,
282
+ options?: INoSqlDbStoppedOperationOptions): Promise<TLocalNoSqlDbStoppedStorageRootAlignmentResult> {
283
+ return LocalNoSqlDb.runStorageRootAlignment(input, options, false, normalizeStoppedStorageRootAlignmentResult);
284
+ }
285
+ /** Abort only the exact incomplete profile append; a complete append must be finished. */
286
+ public static abortStoppedStorageRootAlignment(input: ILocalNoSqlDbStoppedStorageRootAlignmentInput,
287
+ options?: INoSqlDbStoppedOperationOptions): Promise<ILocalNoSqlDbStoppedStorageRootAlignmentAbortResult> {
288
+ return LocalNoSqlDb.runStorageRootAlignment(input, options, true, normalizeStoppedStorageRootAlignmentAbortResult);
289
+ }
290
+ private static async runStorageRootAlignment<TResult>(inputArg: ILocalNoSqlDbStoppedStorageRootAlignmentInput,
291
+ options: INoSqlDbStoppedOperationOptions | undefined, abort: boolean,
292
+ normalize: (value: unknown, input: ILocalNoSqlDbStoppedStorageRootAlignmentInput) => TResult): Promise<TResult> {
293
+ let input: ILocalNoSqlDbStoppedStorageRootAlignmentInput;
294
+ try {
295
+ input = normalizeStoppedStorageRootAlignmentInput(inputArg);
296
+ validateNoSqlDbStoppedOperationOptions(options);
297
+ if (abort && input.dryRun) throw new Error('abort cannot be a dry run');
298
+ } catch { throw new NoSqlDbStoppedOperationError('INVALID_REQUEST'); }
299
+ if (options?.signal?.aborted) throw new NoSqlDbStoppedOperationError('CANCELLED');
300
+ if (!['linux', 'darwin'].includes(process.platform)) throw new NoSqlDbStoppedOperationError('UNSUPPORTED');
301
+ const deadline = Date.now() + (options?.timeoutMs ?? 3_600_000);
302
+ const remaining = () => {
303
+ if (options?.signal?.aborted) throw new NoSqlDbStoppedOperationError('CANCELLED');
304
+ const timeoutMs = deadline - Date.now();
305
+ if (timeoutMs <= 0) throw new NoSqlDbStoppedOperationError('CANCELLED');
306
+ return { signal: options?.signal, timeoutMs };
307
+ };
308
+ // Share the mutation gate and retained ownership with relocation, including
309
+ // retries after an earlier sidecar could not confirm termination.
310
+ return LocalNoSqlDb.withStorageRootRelocationGate(async () => {
311
+ const retained = LocalNoSqlDb.storageRootRelocationBridge;
312
+ if (retained) {
313
+ try { await retained.terminate({ gracePeriodMs: 0 }); }
314
+ catch { throw new NoSqlDbStoppedOperationError('RECOVERY_REQUIRED'); }
315
+ if (!retained.ownershipReleased) throw new NoSqlDbStoppedOperationError('RECOVERY_REQUIRED');
316
+ LocalNoSqlDb.storageRootRelocationBridge = undefined;
317
+ }
318
+ let bridge: RustDbBridge | undefined;
319
+ let result: TResult | undefined;
320
+ let failure: NoSqlDbStoppedOperationError | undefined;
321
+ let dispatched = false;
322
+ const method = abort ? 'abortStoppedStorageRootAlignment' : 'alignStoppedStorageRoot';
323
+ try {
324
+ remaining();
325
+ bridge = LocalNoSqlDb.createStorageRootAlignmentBridge(options?.binaryPath);
326
+ LocalNoSqlDb.storageRootRelocationBridge = bridge;
327
+ if (!await bridge.spawn(remaining())) throw new NoSqlDbStoppedOperationError('IO');
328
+ const commandOptions = remaining();
329
+ dispatched = true;
330
+ const value = abort ? await bridge[rustDbBridgeStorageRootAlignmentAbort](input, commandOptions)
331
+ : await bridge[rustDbBridgeStorageRootAlignment](input, commandOptions);
332
+ result = normalize(value, input);
333
+ } catch (error) {
334
+ if (error instanceof NoSqlDbStoppedOperationError) failure = error;
335
+ else if (isStoppedOperationUnknownMethod(error, method)) failure = new NoSqlDbStoppedOperationError('UNSUPPORTED');
336
+ else if (error instanceof Error && 'code' in error && error.code === 'ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE'
337
+ && 'method' in error && error.method === method && 'responseErrorCode' in error
338
+ && isLocalNoSqlDbStorageRootRelocationErrorCode(error.responseErrorCode)) {
339
+ failure = new NoSqlDbStoppedOperationError(error.responseErrorCode);
340
+ } else failure = new NoSqlDbStoppedOperationError(dispatched ? 'RECOVERY_REQUIRED' : 'IO');
341
+ }
342
+ if (bridge) {
343
+ try {
344
+ await bridge.terminate({ gracePeriodMs: 0 });
345
+ if (!bridge.ownershipReleased) throw new Error('termination unconfirmed');
346
+ if (LocalNoSqlDb.storageRootRelocationBridge === bridge) LocalNoSqlDb.storageRootRelocationBridge = undefined;
347
+ } catch { failure = new NoSqlDbStoppedOperationError('RECOVERY_REQUIRED'); }
348
+ }
349
+ if (failure) throw failure;
350
+ if (result === undefined) throw new NoSqlDbStoppedOperationError('RECOVERY_REQUIRED');
351
+ return result;
352
+ });
353
+ }
354
+
355
+ /** Read-only complete owner audit of a stopped source or physically admissible copy. */
356
+ public static async inspectStoppedStorageRoot(
357
+ inputArg: ILocalNoSqlDbStoppedStorageRootInspectionInput,
358
+ optionsArg?: INoSqlDbStoppedOperationOptions,
359
+ ): Promise<TLocalNoSqlDbStoppedStorageRootInspection> {
360
+ return LocalNoSqlDb.runOfflineInspection(inputArg, optionsArg,
361
+ normalizeStoppedStorageRootInspectionInput,
362
+ (bridge, input, options) => bridge.inspectStoppedStorageRoot(input, options));
363
+ }
364
+
365
+ private static async runOfflineInspection<
366
+ TInput extends { folderPath: string },
367
+ TResult,
368
+ >(
369
+ inputArg: TInput,
370
+ optionsArg: INoSqlDbStoppedOperationOptions | undefined,
371
+ normalizeInputArg: (inputArg: TInput) => TInput,
372
+ inspectArg: (
373
+ bridgeArg: RustDbBridge,
374
+ inputArg: TInput,
375
+ optionsArg?: INoSqlDbStoppedOperationOptions,
376
+ ) => Promise<TResult>,
377
+ ): Promise<TResult> {
378
+ const validatedInput = normalizeInputArg(inputArg);
379
+ validateNoSqlDbStoppedOperationOptions(optionsArg);
380
+ optionsArg?.signal?.throwIfAborted();
381
+
382
+ const input = normalizeInputArg({
383
+ ...validatedInput,
384
+ folderPath: path.resolve(validatedInput.folderPath),
385
+ });
386
+ const deadline = optionsArg?.timeoutMs === undefined
387
+ ? undefined
388
+ : Date.now() + optionsArg.timeoutMs;
389
+ const bridge = LocalNoSqlDb.createOfflineInspectionBridge(optionsArg?.binaryPath);
390
+ let operationFailed = false;
391
+ let operationError: unknown;
392
+ let result: TResult | undefined;
393
+ try {
394
+ let spawnOptions = optionsArg;
395
+ if (deadline !== undefined) {
396
+ const remainingMs = deadline - Date.now();
397
+ if (remainingMs <= 0) {
398
+ throw new Error('NoSQLDB offline inspection timed out before sidecar spawn');
399
+ }
400
+ spawnOptions = {
401
+ signal: optionsArg?.signal,
402
+ timeoutMs: remainingMs,
403
+ };
404
+ }
405
+ if (!await bridge.spawn(spawnOptions)) {
406
+ // The bridge states what it knows: that the engine at its binaryPath did not report ready, or, without one, both meanings.
407
+ throw new Error(bridge.spawnFailureMessage ?? 'the nosqldb engine did not start: spawn() returned false');
408
+ }
409
+ let commandOptions = optionsArg;
410
+ if (deadline !== undefined) {
411
+ const remainingMs = deadline - Date.now();
412
+ if (remainingMs <= 0) {
413
+ throw new Error('NoSQLDB offline inspection timed out while starting the sidecar');
414
+ }
415
+ commandOptions = {
416
+ signal: optionsArg?.signal,
417
+ timeoutMs: remainingMs,
418
+ };
419
+ }
420
+ result = await inspectArg(bridge, input, commandOptions);
421
+ } catch (error) {
422
+ operationFailed = true;
423
+ operationError = error;
424
+ }
425
+
426
+ try {
427
+ await bridge.terminate({ gracePeriodMs: 0 });
428
+ } catch (terminationError) {
429
+ if (operationFailed) {
430
+ throw new AggregateError(
431
+ [operationError, terminationError],
432
+ 'NoSQLDB offline inspection failed and sidecar termination was incomplete',
433
+ { cause: operationError },
434
+ );
435
+ }
436
+ throw terminationError;
437
+ }
438
+ if (operationFailed) {
439
+ throw operationError;
440
+ }
441
+ return result as TResult;
442
+ }
443
+
444
+ /**
445
+ * Inspect physical database and collection names while the owning engine is stopped.
446
+ * This starts only the Rust management sidecar and never initializes storage.
447
+ */
448
+ public static async inspectOfflinePhysicalNamespaces(
449
+ inputArg: ILocalNoSqlDbOfflinePhysicalNamespaceInspection,
450
+ optionsArg?: INoSqlDbStoppedOperationOptions,
451
+ ): Promise<ILocalNoSqlDbOfflinePhysicalNamespaceInspectionResult> {
452
+ return LocalNoSqlDb.runOfflineInspection(
453
+ inputArg,
454
+ optionsArg,
455
+ normalizeLocalNoSqlDbOfflinePhysicalNamespaceInspectionInput,
456
+ (bridgeArg, input, commandOptions) =>
457
+ bridgeArg.inspectOfflinePhysicalNamespaces(input, commandOptions),
458
+ );
459
+ }
460
+
461
+ /**
462
+ * Inspect one exact string value while the owning LocalNoSqlDb engine is stopped.
463
+ * This starts only the Rust management sidecar and never initializes storage.
464
+ */
465
+ public static async inspectOfflineStringValue(
466
+ inputArg: ILocalNoSqlDbOfflineStringValueInspection,
467
+ optionsArg?: INoSqlDbStoppedOperationOptions,
468
+ ): Promise<TLocalNoSqlDbOfflineStringValueInspectionResult> {
469
+ return LocalNoSqlDb.runOfflineInspection(
470
+ inputArg,
471
+ optionsArg,
472
+ normalizeLocalNoSqlDbOfflineInspectionInput,
473
+ (bridgeArg, input, commandOptions) =>
474
+ bridgeArg.inspectOfflineStringValue(input, commandOptions),
475
+ );
476
+ }
477
+
478
+ /**
479
+ * Generate a unique socket path in /tmp
480
+ */
481
+ private generateSocketPath(): string {
482
+ const randomId = crypto.randomBytes(8).toString('hex');
483
+ return path.join(os.tmpdir(), `nosqldb-${randomId}.sock`);
484
+ }
485
+
486
+ /**
487
+ * Check if a Unix socket is alive by attempting to connect.
488
+ */
489
+ private static isSocketAlive(socketPath: string): Promise<boolean> {
490
+ return new Promise((resolve) => {
491
+ const client = net.createConnection({ path: socketPath }, () => {
492
+ client.destroy();
493
+ resolve(true);
494
+ });
495
+ client.on('error', () => {
496
+ resolve(false);
497
+ });
498
+ client.setTimeout(500, () => {
499
+ client.destroy();
500
+ resolve(false);
501
+ });
502
+ });
503
+ }
504
+
505
+ /**
506
+ * Remove stale nosqldb-*.sock files from /tmp.
507
+ * A socket is considered stale if connecting to it fails.
508
+ */
509
+ private static async cleanStaleSockets(): Promise<void> {
510
+ const tmpDir = os.tmpdir();
511
+ let entries: string[];
512
+ try {
513
+ entries = await fs.readdir(tmpDir);
514
+ } catch {
515
+ return;
516
+ }
517
+ const socketFiles = entries.filter(
518
+ (f) => f.startsWith('nosqldb-') && f.endsWith('.sock')
519
+ );
520
+ for (const name of socketFiles) {
521
+ const fullPath = path.join(tmpDir, name);
522
+ try {
523
+ const stat = await fs.stat(fullPath);
524
+ if (!stat.isSocket()) continue;
525
+ const alive = await LocalNoSqlDb.isSocketAlive(fullPath);
526
+ if (!alive) {
527
+ await fs.unlink(fullPath);
528
+ }
529
+ } catch {
530
+ // File may have been removed already; ignore
531
+ }
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Start the local NoSQLDB server and return connection info. A rejection
537
+ * carries `engineStderrTail`: the server's start sets it, and a rejection
538
+ * from before the server's start, which spawned no engine, carries an empty
539
+ * array.
540
+ */
541
+ async start(): Promise<ILocalNoSqlDbConnectionInfo> {
542
+ try {
543
+ return await this.startServer();
544
+ } catch (error) {
545
+ if (!(error !== null && typeof error === 'object' && Object.prototype.hasOwnProperty.call(error, 'engineStderrTail'))) {
546
+ attachNoSqlDbEngineStderrTail(error, []);
547
+ }
548
+ throw error;
549
+ }
550
+ }
551
+
552
+ private async startServer(): Promise<ILocalNoSqlDbConnectionInfo> {
553
+ if (this.server) {
554
+ throw new Error('LocalNoSqlDb is already running');
555
+ }
556
+
557
+ // Use provided socket path or generate one
558
+ this.generatedSocketPath = this.options.socketPath ?? this.generateSocketPath();
559
+
560
+ this.server = new NoSqlDbServer({
561
+ binaryPath: this.options.binaryPath,
562
+ storageRehearsalId: this.options.storageRehearsalId,
563
+ auth: this.options.auth,
564
+ fileStorageStartup: this.options.fileStorageStartup,
565
+ socketPath: this.generatedSocketPath,
566
+ storage: 'file',
567
+ storagePath: this.options.folderPath,
568
+ engineLog: this.options.engineLog,
569
+ onEngineExit: this.options.onEngineExit,
570
+ });
571
+ await this.server.start();
572
+
573
+ return {
574
+ socketPath: this.generatedSocketPath,
575
+ connectionUri: this.server.getConnectionUri(),
576
+ };
577
+ }
578
+
579
+ /**
580
+ * Get connection info (throws if not started)
581
+ */
582
+ getConnectionInfo(): ILocalNoSqlDbConnectionInfo {
583
+ if (!this.server || !this.generatedSocketPath) {
584
+ throw new Error('LocalNoSqlDb is not running. Call start() first.');
585
+ }
586
+ return {
587
+ socketPath: this.generatedSocketPath,
588
+ connectionUri: this.server.getConnectionUri(),
589
+ };
590
+ }
591
+
592
+ /**
593
+ * Get the underlying NoSqlDbServer instance (throws if not started)
594
+ */
595
+ getServer(): NoSqlDbServer {
596
+ if (!this.server) {
597
+ throw new Error('LocalNoSqlDb is not running. Call start() first.');
598
+ }
599
+ return this.server;
600
+ }
601
+
602
+ /**
603
+ * Get the connection URI
604
+ */
605
+ getConnectionUri(): string {
606
+ if (!this.server) {
607
+ throw new Error('LocalNoSqlDb is not running. Call start() first.');
608
+ }
609
+ return this.server.getConnectionUri();
610
+ }
611
+
612
+ /**
613
+ * Check if the server is running
614
+ */
615
+ get running(): boolean {
616
+ return this.server !== null && this.server.running;
617
+ }
618
+
619
+ /**
620
+ * Stop the local NoSQLDB server. Resolves with the result of its
621
+ * `NoSqlDbServer.stop()`, which says how the engine ended; without a server,
622
+ * with a result saying that no stop request was sent.
623
+ */
624
+ async stop(): Promise<INoSqlDbServerStopResult> {
625
+ if (!this.server) return noStopRequestResult;
626
+ const result = await this.server.stop();
627
+ this.server = null;
628
+ this.generatedSocketPath = null;
629
+ return result;
630
+ }
631
+ }
@@ -0,0 +1,19 @@
1
+ export { LocalNoSqlDb } from './classes.localnosqldb.js';
2
+ export type { ILocalNoSqlDbOptions, ILocalNoSqlDbConnectionInfo } from './classes.localnosqldb.js';
3
+ export type {
4
+ ILocalNoSqlDbOfflineInspectionLimits,
5
+ ILocalNoSqlDbOfflinePhysicalNamespaceInspectionLimits,
6
+ ILocalNoSqlDbOfflinePhysicalNamespaceInspection,
7
+ ILocalNoSqlDbOfflinePhysicalNamespaceInspectionResult,
8
+ ILocalNoSqlDbOfflineStringValueInspection,
9
+ TLocalNoSqlDbOfflineStringValueInspectionResult,
10
+ } from '../ts_nosqldb/service-types.js';
11
+ export type {
12
+ ILocalNoSqlDbStoppedStorageRootRelocationInput,
13
+ ILocalNoSqlDbStoppedStorageRootRelocationReceipt,
14
+ TLocalNoSqlDbStorageRootRelocationErrorCode,
15
+ } from '../ts_nosqldb/storage-root-relocation.js';
16
+ export {
17
+ LocalNoSqlDbStorageRootRelocationError,
18
+ localNoSqlDbStorageRootRelocationErrorCodes,
19
+ } from '../ts_nosqldb/storage-root-relocation.js';
@@ -0,0 +1 @@
1
+ // Local module plugins - currently no external dependencies needed