@push.rocks/smartdb 2.15.4 → 2.16.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.
@@ -15,6 +15,30 @@ export interface ISmartDbHealth {
15
15
  collectionCount: number;
16
16
  uptimeSeconds?: number;
17
17
  }
18
+ export interface ILocalSmartDbOfflineInspectionLimits {
19
+ maximumDataFileBytes: number;
20
+ maximumWalFileBytes: number;
21
+ maximumRecords: number;
22
+ maximumRecordBytes: number;
23
+ maximumResultBytes: number;
24
+ }
25
+ export interface ILocalSmartDbOfflineStringValueInspection {
26
+ folderPath: string;
27
+ databaseName: string;
28
+ collectionName: string;
29
+ match: {
30
+ field: string;
31
+ value: string;
32
+ };
33
+ valueField: string;
34
+ limits: ILocalSmartDbOfflineInspectionLimits;
35
+ }
36
+ export type TLocalSmartDbOfflineStringValueInspectionResult = {
37
+ status: 'not-found';
38
+ } | {
39
+ status: 'found';
40
+ value: string;
41
+ };
18
42
  export declare const smartDbResourceFenceErrorCodes: readonly ["EFENCE_REQUIRED", "EFENCE_STALE", "EFENCE_SCOPE_MISMATCH", "EFENCE_UNSUPPORTED", "EFENCE_MUTATION_MISMATCH", "EFENCE_OWNERSHIP_MISMATCH", "EFENCE_STATE_CORRUPT", "EFENCE_RECEIPT_LIMIT", "EFENCE_BUSY", "EFENCE_PUBLICATION_HELD", "EFENCE_CAPABILITY_MISMATCH", "EFENCE_NOT_HELD"];
19
43
  export type TSmartDbResourceFenceErrorCode = (typeof smartDbResourceFenceErrorCodes)[number];
20
44
  export interface ISmartDbResourceFence {
@@ -30,4 +30,4 @@ export const normalizeSmartDbResourceFenceError = (errorArg) => {
30
30
  }
31
31
  return new SmartDbResourceFenceError(match[1], match[2], error);
32
32
  };
33
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS10eXBlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3RzX3NtYXJ0ZGIvc2VydmljZS10eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFrQkEsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUc7SUFDNUMsaUJBQWlCO0lBQ2pCLGNBQWM7SUFDZCx1QkFBdUI7SUFDdkIsb0JBQW9CO0lBQ3BCLDBCQUEwQjtJQUMxQiwyQkFBMkI7SUFDM0Isc0JBQXNCO0lBQ3RCLHNCQUFzQjtJQUN0QixhQUFhO0lBQ2IseUJBQXlCO0lBQ3pCLDRCQUE0QjtJQUM1QixpQkFBaUI7Q0FDVCxDQUFDO0FBc0VYLE1BQU0sT0FBTyx5QkFBMEIsU0FBUSxLQUFLO0lBR2xELFlBQ0UsT0FBdUMsRUFDdkMsVUFBa0IsRUFDbEIsUUFBa0I7UUFFbEIsS0FBSyxDQUFDLEdBQUcsT0FBTyxLQUFLLFVBQVUsRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7UUFDeEQsSUFBSSxDQUFDLElBQUksR0FBRywyQkFBMkIsQ0FBQztRQUN4QyxJQUFJLENBQUMsSUFBSSxHQUFHLE9BQU8sQ0FBQztJQUN0QixDQUFDO0NBQ0Y7QUFFRCxNQUFNLENBQUMsTUFBTSxrQ0FBa0MsR0FBRyxDQUNoRCxRQUFpQixFQUNWLEVBQUU7SUFDVCxNQUFNLEtBQUssR0FDVCxRQUFRLFlBQVksS0FBSztRQUN2QixDQUFDLENBQUMsUUFBUTtRQUNWLENBQUMsQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQztJQUNsQyxNQUFNLEtBQUssR0FBRyw2QkFBNkIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ2hFLElBQ0UsQ0FBQyxLQUFLO1FBQ04sQ0FBQyw4QkFBOEIsQ0FBQyxRQUFRLENBQ3RDLEtBQUssQ0FBQyxDQUFDLENBQW1DLENBQzNDLEVBQ0QsQ0FBQztRQUNELE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUNELE9BQU8sSUFBSSx5QkFBeUIsQ0FDbEMsS0FBSyxDQUFDLENBQUMsQ0FBbUMsRUFDMUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUNSLEtBQUssQ0FDTixDQUFDO0FBQ0osQ0FBQyxDQUFDIn0=
33
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS10eXBlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3RzX3NtYXJ0ZGIvc2VydmljZS10eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUEwQ0EsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUc7SUFDNUMsaUJBQWlCO0lBQ2pCLGNBQWM7SUFDZCx1QkFBdUI7SUFDdkIsb0JBQW9CO0lBQ3BCLDBCQUEwQjtJQUMxQiwyQkFBMkI7SUFDM0Isc0JBQXNCO0lBQ3RCLHNCQUFzQjtJQUN0QixhQUFhO0lBQ2IseUJBQXlCO0lBQ3pCLDRCQUE0QjtJQUM1QixpQkFBaUI7Q0FDVCxDQUFDO0FBc0VYLE1BQU0sT0FBTyx5QkFBMEIsU0FBUSxLQUFLO0lBR2xELFlBQ0UsT0FBdUMsRUFDdkMsVUFBa0IsRUFDbEIsUUFBa0I7UUFFbEIsS0FBSyxDQUFDLEdBQUcsT0FBTyxLQUFLLFVBQVUsRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7UUFDeEQsSUFBSSxDQUFDLElBQUksR0FBRywyQkFBMkIsQ0FBQztRQUN4QyxJQUFJLENBQUMsSUFBSSxHQUFHLE9BQU8sQ0FBQztJQUN0QixDQUFDO0NBQ0Y7QUFFRCxNQUFNLENBQUMsTUFBTSxrQ0FBa0MsR0FBRyxDQUNoRCxRQUFpQixFQUNWLEVBQUU7SUFDVCxNQUFNLEtBQUssR0FDVCxRQUFRLFlBQVksS0FBSztRQUN2QixDQUFDLENBQUMsUUFBUTtRQUNWLENBQUMsQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQztJQUNsQyxNQUFNLEtBQUssR0FBRyw2QkFBNkIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ2hFLElBQ0UsQ0FBQyxLQUFLO1FBQ04sQ0FBQyw4QkFBOEIsQ0FBQyxRQUFRLENBQ3RDLEtBQUssQ0FBQyxDQUFDLENBQW1DLENBQzNDLEVBQ0QsQ0FBQztRQUNELE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUNELE9BQU8sSUFBSSx5QkFBeUIsQ0FDbEMsS0FBSyxDQUFDLENBQUMsQ0FBbUMsRUFDMUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUNSLEtBQUssQ0FDTixDQUFDO0FBQ0osQ0FBQyxDQUFDIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartdb",
3
- "version": "2.15.4",
3
+ "version": "2.16.0",
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": {
package/readme.md CHANGED
@@ -470,6 +470,59 @@ const db = new LocalSmartDb({
470
470
  | `getConnectionUri()` | `string` | Get the connection URI |
471
471
  | `getServer()` | `SmartdbServer` | Access the underlying server |
472
472
  | `running` | `boolean` | Whether the server is running |
473
+ | `LocalSmartDb.inspectOfflineStringValue(input, options?)` | `Promise<TLocalSmartDbOfflineStringValueInspectionResult>` | Read one exact top-level string value from stopped file storage without starting or mutating the engine |
474
+
475
+ #### Offline String-Value Inspection
476
+
477
+ `inspectOfflineStringValue()` is a Linux-only operational API for reading one metadata value while the owning `LocalSmartDb` engine is stopped. Other platforms reject the call because the reader requires Linux `openat2` descriptor traversal. It starts only the Rust management sidecar: it does not start a database listener, run storage migrations, repair tails, replay or truncate the WAL, compact data, or persist hints.
478
+
479
+ ```typescript
480
+ import { LocalSmartDb } from '@push.rocks/smartdb';
481
+
482
+ const result = await LocalSmartDb.inspectOfflineStringValue({
483
+ folderPath: '/var/lib/myapp/smartdb',
484
+ databaseName: 'myapp',
485
+ collectionName: 'MetadataDoc',
486
+ match: {
487
+ field: 'key',
488
+ value: 'migrationStateV1',
489
+ },
490
+ valueField: 'value',
491
+ limits: {
492
+ maximumDataFileBytes: 16 * 1024 * 1024,
493
+ maximumWalFileBytes: 16 * 1024 * 1024,
494
+ maximumRecords: 100_000,
495
+ maximumRecordBytes: 1024 * 1024,
496
+ maximumResultBytes: 1024 * 1024,
497
+ },
498
+ }, {
499
+ timeoutMs: 5000,
500
+ });
501
+
502
+ if (result.status === 'found') {
503
+ console.log(result.value);
504
+ }
505
+ ```
506
+
507
+ The result is only `{ status: 'not-found' }` or `{ status: 'found', value: string }`. No other document fields cross the management boundary. The reader validates descriptor-safe paths, the current data/WAL format, CRCs and live-record semantics, applies any bounded uncommitted WAL overlay in memory, and rejects ambiguous matches, corruption, exceeded limits, symlinks, an engine-owned storage root, or files that change during inspection.
508
+
509
+ The TypeScript API enforces these bounds before creating the sidecar or serializing the IPC request:
510
+
511
+ | Input | Enforced range |
512
+ |---|---|
513
+ | `folderPath` | 1 to 4096 UTF-8 bytes before and after absolute resolution; no control characters |
514
+ | `databaseName`, `collectionName` | 1 to 255 UTF-8 bytes; one canonical path component; no controls, slash, or backslash |
515
+ | `match.field`, `valueField` | 1 to 255 UTF-8 bytes; no control characters |
516
+ | `match.value` | 0 to 1,048,576 UTF-8 bytes and no larger than `maximumRecordBytes` |
517
+ | Complete serialized request | At most 2,097,152 bytes |
518
+ | `timeoutMs` | 1 to 2,147,483,647 milliseconds |
519
+ | `maximumDataFileBytes` | 64 to 268,435,456 bytes |
520
+ | `maximumWalFileBytes` | 64 to 67,108,864 bytes |
521
+ | `maximumRecords` | 1 to 1,000,000 data/WAL items |
522
+ | `maximumRecordBytes` | 22 to 17,825,792 bytes |
523
+ | `maximumResultBytes` | 1 to 16,777,216 bytes |
524
+
525
+ All numeric limits must be positive safe integers. The caller must stop and retain external ownership of the `LocalSmartDb` daemon for the full call. Current storage roots additionally enforce this through SmartDB's storage-owner lock. Roots created before that lock existed rely on the caller's external single-owner coordination plus descriptor and stability validation. One `timeoutMs` budget covers sidecar lookup/spawn, readiness, and the command. Cancellation or timeout kills and reaps the disposable sidecar before the call rejects.
473
526
 
474
527
  ### SmartdbDebugServer
475
528
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdb',
6
- version: '2.15.4',
6
+ version: '2.16.0',
7
7
  description: 'A MongoDB-compatible embedded database server with wire protocol support, backed by a high-performance Rust engine.'
8
8
  }
package/ts/index.ts CHANGED
@@ -5,7 +5,13 @@ export * from './ts_smartdb/index.js';
5
5
 
6
6
  // Export LocalSmartDb
7
7
  export { LocalSmartDb } from './ts_local/index.js';
8
- export type { ILocalSmartDbOptions, ILocalSmartDbConnectionInfo } from './ts_local/index.js';
8
+ export type {
9
+ ILocalSmartDbOptions,
10
+ ILocalSmartDbConnectionInfo,
11
+ ILocalSmartDbOfflineInspectionLimits,
12
+ ILocalSmartDbOfflineStringValueInspection,
13
+ TLocalSmartDbOfflineStringValueInspectionResult,
14
+ } from './ts_local/index.js';
9
15
 
10
16
  // Export migration
11
17
  export {
@@ -3,8 +3,18 @@ import * as fs from 'fs/promises';
3
3
  import * as net from 'net';
4
4
  import * as path from 'path';
5
5
  import * as os from 'os';
6
- import { SmartdbServer } from '../ts_smartdb/index.js';
6
+ import {
7
+ RustDbBridge,
8
+ SmartdbServer,
9
+ type ILocalSmartDbOfflineStringValueInspection,
10
+ type ISmartDbManagementOperationOptions,
11
+ type TLocalSmartDbOfflineStringValueInspectionResult,
12
+ } from '../ts_smartdb/index.js';
7
13
  import { StorageMigrator } from '../ts_migration/index.js';
14
+ import {
15
+ normalizeLocalSmartDbOfflineInspectionInput,
16
+ validateSmartDbManagementOperationOptions,
17
+ } from '../ts_smartdb/offline-inspection.js';
8
18
 
9
19
  /**
10
20
  * Connection information returned by LocalSmartDb.start()
@@ -60,6 +70,86 @@ export class LocalSmartDb {
60
70
  this.options = options;
61
71
  }
62
72
 
73
+ private static createOfflineInspectionBridge(): RustDbBridge {
74
+ return new RustDbBridge();
75
+ }
76
+
77
+ /**
78
+ * Inspect one exact string value while the owning LocalSmartDb engine is stopped.
79
+ * This starts only the Rust management sidecar and never initializes storage.
80
+ */
81
+ public static async inspectOfflineStringValue(
82
+ inputArg: ILocalSmartDbOfflineStringValueInspection,
83
+ optionsArg?: ISmartDbManagementOperationOptions,
84
+ ): Promise<TLocalSmartDbOfflineStringValueInspectionResult> {
85
+ const validatedInput = normalizeLocalSmartDbOfflineInspectionInput(inputArg);
86
+ validateSmartDbManagementOperationOptions(optionsArg);
87
+ optionsArg?.signal?.throwIfAborted();
88
+
89
+ const input = normalizeLocalSmartDbOfflineInspectionInput({
90
+ ...validatedInput,
91
+ folderPath: path.resolve(validatedInput.folderPath),
92
+ });
93
+ const deadline = optionsArg?.timeoutMs === undefined
94
+ ? undefined
95
+ : Date.now() + optionsArg.timeoutMs;
96
+ const bridge = LocalSmartDb.createOfflineInspectionBridge();
97
+ let operationFailed = false;
98
+ let operationError: unknown;
99
+ let result: TLocalSmartDbOfflineStringValueInspectionResult | undefined;
100
+ try {
101
+ let spawnOptions = optionsArg;
102
+ if (deadline !== undefined) {
103
+ const remainingMs = deadline - Date.now();
104
+ if (remainingMs <= 0) {
105
+ throw new Error('SmartDB offline inspection timed out before sidecar spawn');
106
+ }
107
+ spawnOptions = {
108
+ signal: optionsArg?.signal,
109
+ timeoutMs: remainingMs,
110
+ };
111
+ }
112
+ if (!await bridge.spawn(spawnOptions)) {
113
+ throw new Error(
114
+ 'smartdb Rust binary not found. Set SMARTDB_RUST_BINARY env var, ' +
115
+ 'install the platform package, or build locally with `tsrust`.',
116
+ );
117
+ }
118
+ let commandOptions = optionsArg;
119
+ if (deadline !== undefined) {
120
+ const remainingMs = deadline - Date.now();
121
+ if (remainingMs <= 0) {
122
+ throw new Error('SmartDB offline inspection timed out while starting the sidecar');
123
+ }
124
+ commandOptions = {
125
+ signal: optionsArg?.signal,
126
+ timeoutMs: remainingMs,
127
+ };
128
+ }
129
+ result = await bridge.inspectOfflineStringValue(input, commandOptions);
130
+ } catch (error) {
131
+ operationFailed = true;
132
+ operationError = error;
133
+ }
134
+
135
+ try {
136
+ await bridge.terminate({ gracePeriodMs: 0 });
137
+ } catch (terminationError) {
138
+ if (operationFailed) {
139
+ throw new AggregateError(
140
+ [operationError, terminationError],
141
+ 'SmartDB offline inspection failed and sidecar termination was incomplete',
142
+ { cause: operationError },
143
+ );
144
+ }
145
+ throw terminationError;
146
+ }
147
+ if (operationFailed) {
148
+ throw operationError;
149
+ }
150
+ return result as TLocalSmartDbOfflineStringValueInspectionResult;
151
+ }
152
+
63
153
  /**
64
154
  * Generate a unique socket path in /tmp
65
155
  */
@@ -1,2 +1,7 @@
1
1
  export { LocalSmartDb } from './classes.localsmartdb.js';
2
2
  export type { ILocalSmartDbOptions, ILocalSmartDbConnectionInfo } from './classes.localsmartdb.js';
3
+ export type {
4
+ ILocalSmartDbOfflineInspectionLimits,
5
+ ILocalSmartDbOfflineStringValueInspection,
6
+ TLocalSmartDbOfflineStringValueInspectionResult,
7
+ } from '../ts_smartdb/service-types.js';
@@ -36,6 +36,9 @@ export type {
36
36
  ISmartDbCommitDatabasePublicationInput,
37
37
  ISmartDbManagementOperationOptions,
38
38
  ISmartDbDatabaseResourceFenceState,
39
+ ILocalSmartDbOfflineInspectionLimits,
40
+ ILocalSmartDbOfflineStringValueInspection,
41
+ TLocalSmartDbOfflineStringValueInspectionResult,
39
42
  TSmartDbCommitDatabasePublicationResult,
40
43
  TSmartDbResourceFenceErrorCode,
41
44
  ISmartDbDatabaseTenantInput,
@@ -0,0 +1,241 @@
1
+ import * as plugins from './plugins.js';
2
+ import type {
3
+ ILocalSmartDbOfflineStringValueInspection,
4
+ ISmartDbManagementOperationOptions,
5
+ } from './service-types.js';
6
+
7
+ export const localSmartDbOfflineInspectionBounds = {
8
+ maximumFolderPathBytes: 4096,
9
+ maximumNamespaceComponentBytes: 255,
10
+ maximumFieldNameBytes: 255,
11
+ maximumMatchValueBytes: 1024 * 1024,
12
+ maximumRequestBytes: 2 * 1024 * 1024,
13
+ maximumTimeoutMs: 2_147_483_647,
14
+ limits: {
15
+ maximumDataFileBytes: { minimum: 64, maximum: 256 * 1024 * 1024 },
16
+ maximumWalFileBytes: { minimum: 64, maximum: 64 * 1024 * 1024 },
17
+ maximumRecords: { minimum: 1, maximum: 1_000_000 },
18
+ maximumRecordBytes: { minimum: 22, maximum: 17 * 1024 * 1024 },
19
+ maximumResultBytes: { minimum: 1, maximum: 16 * 1024 * 1024 },
20
+ },
21
+ } as const;
22
+
23
+ const inputKeys = new Set([
24
+ 'folderPath',
25
+ 'databaseName',
26
+ 'collectionName',
27
+ 'match',
28
+ 'valueField',
29
+ 'limits',
30
+ ]);
31
+ const matchKeys = new Set(['field', 'value']);
32
+ const limitKeys = [
33
+ 'maximumDataFileBytes',
34
+ 'maximumWalFileBytes',
35
+ 'maximumRecords',
36
+ 'maximumRecordBytes',
37
+ 'maximumResultBytes',
38
+ ] as const;
39
+ const limitKeySet = new Set<string>(limitKeys);
40
+ const requestStructuralBytes = plugins.buffer.Buffer.byteLength(
41
+ '{"folderPath":,"databaseName":,"collectionName":,"match":{"field":,"value":},"valueField":,"limits":{"maximumDataFileBytes":,"maximumWalFileBytes":,"maximumRecords":,"maximumRecordBytes":,"maximumResultBytes":}}',
42
+ 'utf8',
43
+ );
44
+ const requestEnvelopeBytes = plugins.buffer.Buffer.byteLength(
45
+ '{"id":"req_9007199254740991","method":"inspectOfflineStringValue","params":}',
46
+ 'utf8',
47
+ );
48
+
49
+ const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> =>
50
+ typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
51
+
52
+ const rejectUnknownEnumerableKeys = (
53
+ valueArg: object,
54
+ allowedKeysArg: Set<string>,
55
+ labelArg: string,
56
+ ): void => {
57
+ for (const key in valueArg) {
58
+ if (!allowedKeysArg.has(key)) {
59
+ throw new TypeError(`LocalSmartDb offline inspection ${labelArg} contains unknown key '${key}'`);
60
+ }
61
+ }
62
+ };
63
+
64
+ const encodedJsonStringBytes = (valueArg: string): number => {
65
+ let bytes = 2;
66
+ for (let index = 0; index < valueArg.length; index++) {
67
+ const code = valueArg.charCodeAt(index);
68
+ if (
69
+ code === 0x22
70
+ || code === 0x5c
71
+ || code === 0x08
72
+ || code === 0x09
73
+ || code === 0x0a
74
+ || code === 0x0c
75
+ || code === 0x0d
76
+ ) {
77
+ bytes += 2;
78
+ } else if (code <= 0x1f) {
79
+ bytes += 6;
80
+ } else if (code <= 0x7f) {
81
+ bytes += 1;
82
+ } else if (code <= 0x7ff) {
83
+ bytes += 2;
84
+ } else if (code >= 0xd800 && code <= 0xdbff) {
85
+ const next = valueArg.charCodeAt(index + 1);
86
+ if (next >= 0xdc00 && next <= 0xdfff) {
87
+ bytes += 4;
88
+ index++;
89
+ } else {
90
+ bytes += 6;
91
+ }
92
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
93
+ bytes += 6;
94
+ } else {
95
+ bytes += 3;
96
+ }
97
+ }
98
+ return bytes;
99
+ };
100
+
101
+ const validateString = (
102
+ valueArg: unknown,
103
+ labelArg: string,
104
+ minimumBytesArg: number,
105
+ maximumBytesArg: number,
106
+ rejectControlsArg: boolean,
107
+ ): void => {
108
+ if (typeof valueArg !== 'string') {
109
+ throw new TypeError(`LocalSmartDb offline inspection ${labelArg} must be a string`);
110
+ }
111
+ const bytes = plugins.buffer.Buffer.byteLength(valueArg, 'utf8');
112
+ if (bytes < minimumBytesArg || bytes > maximumBytesArg) {
113
+ throw new RangeError(
114
+ `LocalSmartDb offline inspection ${labelArg} must be between ${minimumBytesArg} and ${maximumBytesArg} UTF-8 bytes`,
115
+ );
116
+ }
117
+ if (rejectControlsArg) {
118
+ for (const character of valueArg) {
119
+ if (/\p{Cc}/u.test(character)) {
120
+ throw new TypeError(
121
+ `LocalSmartDb offline inspection ${labelArg} must not contain control characters`,
122
+ );
123
+ }
124
+ }
125
+ }
126
+ };
127
+
128
+ export const normalizeLocalSmartDbOfflineInspectionInput = (
129
+ inputArg: ILocalSmartDbOfflineStringValueInspection,
130
+ ): ILocalSmartDbOfflineStringValueInspection => {
131
+ if (!isRecord(inputArg as unknown)) {
132
+ throw new TypeError('LocalSmartDb offline inspection input must be an object');
133
+ }
134
+ rejectUnknownEnumerableKeys(inputArg, inputKeys, 'input');
135
+ const inputRecord = inputArg as unknown as Record<string, unknown>;
136
+ const match = inputRecord.match;
137
+ if (!isRecord(match)) {
138
+ throw new TypeError('LocalSmartDb offline inspection match must be an object');
139
+ }
140
+ rejectUnknownEnumerableKeys(match, matchKeys, 'match');
141
+ const limits = inputRecord.limits;
142
+ if (!isRecord(limits)) {
143
+ throw new TypeError('LocalSmartDb offline inspection limits must be an object');
144
+ }
145
+ rejectUnknownEnumerableKeys(limits, limitKeySet, 'limits');
146
+
147
+ const normalized = {
148
+ folderPath: inputRecord.folderPath,
149
+ databaseName: inputRecord.databaseName,
150
+ collectionName: inputRecord.collectionName,
151
+ match: {
152
+ field: match.field,
153
+ value: match.value,
154
+ },
155
+ valueField: inputRecord.valueField,
156
+ limits: {
157
+ maximumDataFileBytes: limits.maximumDataFileBytes,
158
+ maximumWalFileBytes: limits.maximumWalFileBytes,
159
+ maximumRecords: limits.maximumRecords,
160
+ maximumRecordBytes: limits.maximumRecordBytes,
161
+ maximumResultBytes: limits.maximumResultBytes,
162
+ },
163
+ } as unknown as ILocalSmartDbOfflineStringValueInspection;
164
+
165
+ const bounds = localSmartDbOfflineInspectionBounds;
166
+ validateString(normalized.folderPath, 'folderPath', 1, bounds.maximumFolderPathBytes, true);
167
+ for (const [label, value] of [
168
+ ['databaseName', normalized.databaseName],
169
+ ['collectionName', normalized.collectionName],
170
+ ] as const) {
171
+ validateString(value, label, 1, bounds.maximumNamespaceComponentBytes, true);
172
+ if (value === '.' || value === '..' || value.includes('/') || value.includes('\\')) {
173
+ throw new TypeError(
174
+ `LocalSmartDb offline inspection ${label} must be one canonical path component`,
175
+ );
176
+ }
177
+ }
178
+ validateString(normalized.match.field, 'match.field', 1, bounds.maximumFieldNameBytes, true);
179
+ validateString(normalized.valueField, 'valueField', 1, bounds.maximumFieldNameBytes, true);
180
+ validateString(
181
+ normalized.match.value,
182
+ 'match.value',
183
+ 0,
184
+ bounds.maximumMatchValueBytes,
185
+ false,
186
+ );
187
+
188
+ for (const key of limitKeys) {
189
+ const value = normalized.limits[key];
190
+ const range = bounds.limits[key as keyof typeof bounds.limits];
191
+ if (!Number.isSafeInteger(value) || value < range.minimum || value > range.maximum) {
192
+ throw new RangeError(
193
+ `LocalSmartDb offline inspection limits.${key} must be a safe integer between ${range.minimum} and ${range.maximum}`,
194
+ );
195
+ }
196
+ }
197
+ if (plugins.buffer.Buffer.byteLength(normalized.match.value, 'utf8') > normalized.limits.maximumRecordBytes) {
198
+ throw new RangeError(
199
+ 'LocalSmartDb offline inspection match.value must not exceed limits.maximumRecordBytes',
200
+ );
201
+ }
202
+
203
+ let requestBytes = requestEnvelopeBytes
204
+ + requestStructuralBytes
205
+ + encodedJsonStringBytes(normalized.folderPath)
206
+ + encodedJsonStringBytes(normalized.databaseName)
207
+ + encodedJsonStringBytes(normalized.collectionName)
208
+ + encodedJsonStringBytes(normalized.match.field)
209
+ + encodedJsonStringBytes(normalized.match.value)
210
+ + encodedJsonStringBytes(normalized.valueField);
211
+ for (const key of limitKeys) {
212
+ requestBytes += String(normalized.limits[key]).length;
213
+ }
214
+ if (requestBytes > bounds.maximumRequestBytes) {
215
+ throw new RangeError(
216
+ `LocalSmartDb offline inspection request exceeds the ${bounds.maximumRequestBytes}-byte serialized limit`,
217
+ );
218
+ }
219
+ return normalized;
220
+ };
221
+
222
+ export const validateSmartDbManagementOperationOptions = (
223
+ optionsArg?: ISmartDbManagementOperationOptions,
224
+ ): void => {
225
+ if (optionsArg === undefined) return;
226
+ if (!isRecord(optionsArg as unknown)) {
227
+ throw new TypeError('SmartDB management operation options must be an object');
228
+ }
229
+ if (
230
+ optionsArg.timeoutMs !== undefined
231
+ && (
232
+ !Number.isSafeInteger(optionsArg.timeoutMs)
233
+ || optionsArg.timeoutMs <= 0
234
+ || optionsArg.timeoutMs > localSmartDbOfflineInspectionBounds.maximumTimeoutMs
235
+ )
236
+ ) {
237
+ throw new RangeError(
238
+ `SmartDB management operation timeoutMs must be an integer between 1 and ${localSmartDbOfflineInspectionBounds.maximumTimeoutMs}`,
239
+ );
240
+ }
241
+ };
@@ -3,6 +3,10 @@ import * as path from 'path';
3
3
  import * as url from 'url';
4
4
  import { EventEmitter } from 'events';
5
5
  import { normalizeSmartDbResourceFenceError } from './service-types.js';
6
+ import {
7
+ normalizeLocalSmartDbOfflineInspectionInput,
8
+ validateSmartDbManagementOperationOptions,
9
+ } from './offline-inspection.js';
6
10
  import type {
7
11
  ISmartDbHealth,
8
12
  ISmartDbDatabaseTenantInput,
@@ -19,6 +23,8 @@ import type {
19
23
  ISmartDbCommitDatabasePublicationInput,
20
24
  ISmartDbManagementOperationOptions,
21
25
  ISmartDbDatabaseResourceFenceState,
26
+ ILocalSmartDbOfflineStringValueInspection,
27
+ TLocalSmartDbOfflineStringValueInspectionResult,
22
28
  TSmartDbCommitDatabasePublicationResult,
23
29
  } from './service-types.js';
24
30
 
@@ -39,6 +45,9 @@ export type {
39
45
  ISmartDbCommitDatabasePublicationInput,
40
46
  ISmartDbManagementOperationOptions,
41
47
  ISmartDbDatabaseResourceFenceState,
48
+ ILocalSmartDbOfflineInspectionLimits,
49
+ ILocalSmartDbOfflineStringValueInspection,
50
+ TLocalSmartDbOfflineStringValueInspectionResult,
42
51
  TSmartDbCommitDatabasePublicationResult,
43
52
  } from './service-types.js';
44
53
 
@@ -139,6 +148,10 @@ export interface ISmartDbMetrics {
139
148
  * Type-safe command definitions for the RustDb IPC protocol.
140
149
  */
141
150
  type TSmartDbCommands = {
151
+ inspectOfflineStringValue: {
152
+ params: ILocalSmartDbOfflineStringValueInspection;
153
+ result: TLocalSmartDbOfflineStringValueInspectionResult;
154
+ };
142
155
  transitionAuthMetadataPermissions: {
143
156
  params: { usersPath: string; expectedMode: number; targetMode: number };
144
157
  result: { status: TAuthMetadataPermissionTransitionStatus };
@@ -334,13 +347,70 @@ export class RustDbBridge extends EventEmitter {
334
347
  * Spawn the Rust binary in management mode.
335
348
  * Returns true if the binary was found and spawned successfully.
336
349
  */
337
- public async spawn(): Promise<boolean> {
338
- const spawned = await this.bridge.spawn();
339
- if (spawned) {
340
- this.terminationConfirmed = false;
341
- this.terminationFailure = undefined;
350
+ public async spawn(optionsArg?: ISmartDbManagementOperationOptions): Promise<boolean> {
351
+ validateSmartDbManagementOperationOptions(optionsArg);
352
+ if (optionsArg?.signal?.aborted) {
353
+ throw new plugins.smartrust.RustBridgeRequestError(
354
+ 'SmartDB Rust sidecar spawn was aborted before it started',
355
+ {
356
+ code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
357
+ method: 'spawn',
358
+ cause: optionsArg.signal.reason,
359
+ },
360
+ );
361
+ }
362
+
363
+ this.terminationConfirmed = false;
364
+ this.terminationFailure = undefined;
365
+ const spawnPromise = this.bridge.spawn();
366
+ let timeout: ReturnType<typeof setTimeout> | undefined;
367
+ let abortHandler: (() => void) | undefined;
368
+ const cancellationPromise = new Promise<never>((_resolveArg, rejectArg) => {
369
+ if (optionsArg?.timeoutMs !== undefined) {
370
+ timeout = setTimeout(() => {
371
+ rejectArg(new plugins.smartrust.RustBridgeRequestError(
372
+ `SmartDB Rust sidecar spawn timed out after ${optionsArg.timeoutMs}ms`,
373
+ {
374
+ code: 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT',
375
+ method: 'spawn',
376
+ },
377
+ ));
378
+ }, optionsArg.timeoutMs);
379
+ }
380
+ if (optionsArg?.signal) {
381
+ abortHandler = () => {
382
+ rejectArg(new plugins.smartrust.RustBridgeRequestError(
383
+ 'SmartDB Rust sidecar spawn was aborted',
384
+ {
385
+ code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
386
+ method: 'spawn',
387
+ cause: optionsArg.signal!.reason,
388
+ },
389
+ ));
390
+ };
391
+ optionsArg.signal.addEventListener('abort', abortHandler, { once: true });
392
+ }
393
+ });
394
+
395
+ try {
396
+ const spawned = await Promise.race([spawnPromise, cancellationPromise]);
397
+ if (!spawned) {
398
+ await this.terminate({ gracePeriodMs: 0 });
399
+ }
400
+ return spawned;
401
+ } catch (error) {
402
+ try {
403
+ await this.terminate({ gracePeriodMs: 0 });
404
+ } catch (terminationError) {
405
+ throw new SmartDbManagementOperationTerminationError(error, terminationError);
406
+ }
407
+ throw error;
408
+ } finally {
409
+ if (timeout) clearTimeout(timeout);
410
+ if (abortHandler && optionsArg?.signal) {
411
+ optionsArg.signal.removeEventListener('abort', abortHandler);
412
+ }
342
413
  }
343
- return spawned;
344
414
  }
345
415
 
346
416
  /**
@@ -431,6 +501,24 @@ export class RustDbBridge extends EventEmitter {
431
501
 
432
502
  // --- Convenience methods for each management command ---
433
503
 
504
+ public async inspectOfflineStringValue(
505
+ inputArg: ILocalSmartDbOfflineStringValueInspection,
506
+ optionsArg?: ISmartDbManagementOperationOptions,
507
+ ): Promise<TLocalSmartDbOfflineStringValueInspectionResult> {
508
+ const input = normalizeLocalSmartDbOfflineInspectionInput(inputArg);
509
+ validateSmartDbManagementOperationOptions(optionsArg);
510
+ try {
511
+ return await this.bridge.sendCommand(
512
+ 'inspectOfflineStringValue',
513
+ input,
514
+ optionsArg,
515
+ ) as TLocalSmartDbOfflineStringValueInspectionResult;
516
+ } catch (error) {
517
+ await this.awaitManagementOperationTermination(error);
518
+ throw error;
519
+ }
520
+ }
521
+
434
522
  public async transitionAuthMetadataPermissions(
435
523
  usersPathArg: string,
436
524
  expectedModeArg: number,
@@ -16,6 +16,30 @@ export interface ISmartDbHealth {
16
16
  uptimeSeconds?: number;
17
17
  }
18
18
 
19
+ export interface ILocalSmartDbOfflineInspectionLimits {
20
+ maximumDataFileBytes: number;
21
+ maximumWalFileBytes: number;
22
+ maximumRecords: number;
23
+ maximumRecordBytes: number;
24
+ maximumResultBytes: number;
25
+ }
26
+
27
+ export interface ILocalSmartDbOfflineStringValueInspection {
28
+ folderPath: string;
29
+ databaseName: string;
30
+ collectionName: string;
31
+ match: {
32
+ field: string;
33
+ value: string;
34
+ };
35
+ valueField: string;
36
+ limits: ILocalSmartDbOfflineInspectionLimits;
37
+ }
38
+
39
+ export type TLocalSmartDbOfflineStringValueInspectionResult =
40
+ | { status: 'not-found' }
41
+ | { status: 'found'; value: string };
42
+
19
43
  export const smartDbResourceFenceErrorCodes = [
20
44
  'EFENCE_REQUIRED',
21
45
  'EFENCE_STALE',