@push.rocks/smartdb 5.0.5 → 5.1.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.
@@ -0,0 +1,27 @@
1
+ export declare const localSmartDbStorageRootRelocationErrorCodes: readonly ["INVALID_REQUEST", "UNSUPPORTED", "BUSY", "NOT_FOUND", "DESTINATION_CONFLICT", "IDENTITY_MISMATCH", "STATE_CORRUPT", "IO", "RECOVERY_REQUIRED"];
2
+ export type TLocalSmartDbStorageRootRelocationErrorCode = (typeof localSmartDbStorageRootRelocationErrorCodes)[number];
3
+ export interface ILocalSmartDbStoppedStorageRootRelocationInput {
4
+ sourceFolderPath: string;
5
+ destinationFolderPath: string;
6
+ relocationId: string;
7
+ }
8
+ export interface ILocalSmartDbStoppedStorageRootRelocationReceipt {
9
+ format: 'smartdb.storage-root-relocation.receipt.v1';
10
+ version: 1;
11
+ relocationId: string;
12
+ sourceFolderPath: string;
13
+ destinationFolderPath: string;
14
+ providerRootId: string;
15
+ storageRootDevice: string;
16
+ storageRootInode: string;
17
+ receiptSha256: string;
18
+ sourceReceiptRetained: true;
19
+ }
20
+ export declare class LocalSmartDbStorageRootRelocationError extends Error {
21
+ readonly code: TLocalSmartDbStorageRootRelocationErrorCode;
22
+ constructor(codeArg: TLocalSmartDbStorageRootRelocationErrorCode);
23
+ }
24
+ export declare const normalizeLocalSmartDbStoppedStorageRootRelocationInput: (inputArg: ILocalSmartDbStoppedStorageRootRelocationInput) => ILocalSmartDbStoppedStorageRootRelocationInput;
25
+ export declare const normalizeLocalSmartDbStoppedStorageRootRelocationReceipt: (valueArg: unknown, inputArg: ILocalSmartDbStoppedStorageRootRelocationInput) => ILocalSmartDbStoppedStorageRootRelocationReceipt;
26
+ export declare const assertNoLocalSmartDbStorageRootRelocationWorkspace: (storagePathArg: string) => Promise<void>;
27
+ export declare const isLocalSmartDbStorageRootRelocationErrorCode: (valueArg: unknown) => valueArg is TLocalSmartDbStorageRootRelocationErrorCode;
@@ -0,0 +1,211 @@
1
+ import * as plugins from './plugins.js';
2
+ export const localSmartDbStorageRootRelocationErrorCodes = [
3
+ 'INVALID_REQUEST',
4
+ 'UNSUPPORTED',
5
+ 'BUSY',
6
+ 'NOT_FOUND',
7
+ 'DESTINATION_CONFLICT',
8
+ 'IDENTITY_MISMATCH',
9
+ 'STATE_CORRUPT',
10
+ 'IO',
11
+ 'RECOVERY_REQUIRED',
12
+ ];
13
+ export class LocalSmartDbStorageRootRelocationError extends Error {
14
+ constructor(codeArg) {
15
+ super(`SmartDB stopped storage-root relocation failed with code ${codeArg}`);
16
+ this.name = 'LocalSmartDbStorageRootRelocationError';
17
+ this.code = codeArg;
18
+ }
19
+ }
20
+ const relocationInputKeys = [
21
+ 'sourceFolderPath',
22
+ 'destinationFolderPath',
23
+ 'relocationId',
24
+ ];
25
+ const relocationReceiptKeys = [
26
+ 'format',
27
+ 'version',
28
+ 'relocationId',
29
+ 'sourceFolderPath',
30
+ 'destinationFolderPath',
31
+ 'providerRootId',
32
+ 'storageRootDevice',
33
+ 'storageRootInode',
34
+ 'receiptSha256',
35
+ 'sourceReceiptRetained',
36
+ ];
37
+ const maximumFolderPathBytes = 4095;
38
+ const maximumRelocationIdBytes = 256;
39
+ const maximumU64Decimal = '18446744073709551615';
40
+ const receiptDigestDomain = 'smartdb.storage-root-relocation.receipt.v1\0';
41
+ const relocationError = (codeArg) => new LocalSmartDbStorageRootRelocationError(codeArg);
42
+ const isPlainExactDataObject = (valueArg, keysArg) => {
43
+ if (typeof valueArg !== 'object'
44
+ || valueArg === null
45
+ || Array.isArray(valueArg)
46
+ || Object.getPrototypeOf(valueArg) !== Object.prototype) {
47
+ return false;
48
+ }
49
+ const ownKeys = Reflect.ownKeys(valueArg);
50
+ if (ownKeys.length !== keysArg.length
51
+ || ownKeys.some((keyArg) => typeof keyArg !== 'string' || !keysArg.includes(keyArg))) {
52
+ return false;
53
+ }
54
+ const descriptors = Object.getOwnPropertyDescriptors(valueArg);
55
+ return keysArg.every((keyArg) => {
56
+ const descriptor = descriptors[keyArg];
57
+ return descriptor !== undefined
58
+ && descriptor.enumerable
59
+ && Object.prototype.hasOwnProperty.call(descriptor, 'value');
60
+ });
61
+ };
62
+ const isValidUnicodeScalarString = (valueArg) => {
63
+ for (let index = 0; index < valueArg.length; index++) {
64
+ const code = valueArg.charCodeAt(index);
65
+ if (code >= 0xd800 && code <= 0xdbff) {
66
+ const next = valueArg.charCodeAt(index + 1);
67
+ if (next < 0xdc00 || next > 0xdfff)
68
+ return false;
69
+ index++;
70
+ }
71
+ else if (code >= 0xdc00 && code <= 0xdfff) {
72
+ return false;
73
+ }
74
+ }
75
+ return true;
76
+ };
77
+ const isNestedPath = (parentArg, childArg) => {
78
+ const relative = plugins.path.relative(parentArg, childArg);
79
+ return relative !== ''
80
+ && relative !== '..'
81
+ && !relative.startsWith(`..${plugins.path.sep}`)
82
+ && !plugins.path.isAbsolute(relative);
83
+ };
84
+ const normalizeFolderPath = (valueArg) => {
85
+ if (typeof valueArg !== 'string'
86
+ || !plugins.path.isAbsolute(valueArg)
87
+ || !isValidUnicodeScalarString(valueArg)
88
+ || [...valueArg].some(characterArg => /\p{Cc}/u.test(characterArg))) {
89
+ throw relocationError('INVALID_REQUEST');
90
+ }
91
+ const initialBytes = plugins.buffer.Buffer.byteLength(valueArg, 'utf8');
92
+ if (initialBytes < 1 || initialBytes > maximumFolderPathBytes) {
93
+ throw relocationError('INVALID_REQUEST');
94
+ }
95
+ const normalized = plugins.path.resolve(valueArg);
96
+ const normalizedBytes = plugins.buffer.Buffer.byteLength(normalized, 'utf8');
97
+ if (normalizedBytes < 1
98
+ || normalizedBytes > maximumFolderPathBytes
99
+ || plugins.path.parse(normalized).root === normalized
100
+ || !isValidUnicodeScalarString(normalized)
101
+ || [...normalized].some(characterArg => /\p{Cc}/u.test(characterArg))) {
102
+ throw relocationError('INVALID_REQUEST');
103
+ }
104
+ return normalized;
105
+ };
106
+ export const normalizeLocalSmartDbStoppedStorageRootRelocationInput = (inputArg) => {
107
+ if (!isPlainExactDataObject(inputArg, relocationInputKeys)) {
108
+ throw relocationError('INVALID_REQUEST');
109
+ }
110
+ const sourceFolderPath = normalizeFolderPath(inputArg.sourceFolderPath);
111
+ const destinationFolderPath = normalizeFolderPath(inputArg.destinationFolderPath);
112
+ const sourceFolderPathBytes = plugins.buffer.Buffer.byteLength(sourceFolderPath, 'utf8');
113
+ const destinationFolderPathBytes = plugins.buffer.Buffer.byteLength(destinationFolderPath, 'utf8');
114
+ if (sourceFolderPath === destinationFolderPath
115
+ || isNestedPath(sourceFolderPath, destinationFolderPath)
116
+ || isNestedPath(destinationFolderPath, sourceFolderPath)
117
+ || destinationFolderPathBytes > sourceFolderPathBytes) {
118
+ throw relocationError('INVALID_REQUEST');
119
+ }
120
+ if (typeof inputArg.relocationId !== 'string'
121
+ || plugins.buffer.Buffer.byteLength(inputArg.relocationId, 'utf8') > maximumRelocationIdBytes
122
+ || !/^[A-Za-z0-9][A-Za-z0-9._:@-]*$/.test(inputArg.relocationId)) {
123
+ throw relocationError('INVALID_REQUEST');
124
+ }
125
+ return {
126
+ sourceFolderPath,
127
+ destinationFolderPath,
128
+ relocationId: inputArg.relocationId,
129
+ };
130
+ };
131
+ const receiptDigest = (receiptArg) => plugins.crypto
132
+ .createHash('sha256')
133
+ .update(receiptDigestDomain, 'utf8')
134
+ .update(JSON.stringify(receiptArg), 'utf8')
135
+ .digest('hex');
136
+ const isCanonicalU64Decimal = (valueArg) => typeof valueArg === 'string'
137
+ && /^(?:0|[1-9][0-9]{0,19})$/.test(valueArg)
138
+ && (valueArg.length < 20 || valueArg <= maximumU64Decimal);
139
+ const isLowercaseSha256 = (valueArg) => typeof valueArg === 'string' && /^[0-9a-f]{64}$/.test(valueArg);
140
+ export const normalizeLocalSmartDbStoppedStorageRootRelocationReceipt = (valueArg, inputArg) => {
141
+ if (!isPlainExactDataObject(valueArg, relocationReceiptKeys)) {
142
+ throw relocationError('RECOVERY_REQUIRED');
143
+ }
144
+ const receipt = valueArg;
145
+ if (receipt.format !== 'smartdb.storage-root-relocation.receipt.v1'
146
+ || receipt.version !== 1
147
+ || receipt.relocationId !== inputArg.relocationId
148
+ || receipt.sourceFolderPath !== inputArg.sourceFolderPath
149
+ || receipt.destinationFolderPath !== inputArg.destinationFolderPath
150
+ || !isLowercaseSha256(receipt.providerRootId)
151
+ || !isCanonicalU64Decimal(receipt.storageRootDevice)
152
+ || !isCanonicalU64Decimal(receipt.storageRootInode)
153
+ || !isLowercaseSha256(receipt.receiptSha256)
154
+ || receipt.sourceReceiptRetained !== true) {
155
+ throw relocationError('RECOVERY_REQUIRED');
156
+ }
157
+ const withoutDigest = {
158
+ format: receipt.format,
159
+ version: receipt.version,
160
+ relocationId: receipt.relocationId,
161
+ sourceFolderPath: receipt.sourceFolderPath,
162
+ destinationFolderPath: receipt.destinationFolderPath,
163
+ providerRootId: receipt.providerRootId,
164
+ storageRootDevice: receipt.storageRootDevice,
165
+ storageRootInode: receipt.storageRootInode,
166
+ sourceReceiptRetained: receipt.sourceReceiptRetained,
167
+ };
168
+ if (receiptDigest(withoutDigest) !== receipt.receiptSha256) {
169
+ throw relocationError('RECOVERY_REQUIRED');
170
+ }
171
+ return { ...withoutDigest, receiptSha256: receipt.receiptSha256 };
172
+ };
173
+ export const assertNoLocalSmartDbStorageRootRelocationWorkspace = async (storagePathArg) => {
174
+ const storagePath = plugins.path.resolve(storagePathArg);
175
+ const internalPath = plugins.path.join(storagePath, '.__rustdb_internal');
176
+ const workspacePath = plugins.path.join(internalPath, 'storage-root-relocation');
177
+ try {
178
+ const storage = await plugins.fs.stat(storagePath);
179
+ if (!storage.isDirectory()) {
180
+ throw new Error('not a directory');
181
+ }
182
+ }
183
+ catch (error) {
184
+ if (error.code === 'ENOENT')
185
+ return;
186
+ throw new Error('SmartDB storage-root relocation workspace inspection is ambiguous');
187
+ }
188
+ try {
189
+ const internal = await plugins.fs.lstat(internalPath);
190
+ if (internal.isSymbolicLink() || !internal.isDirectory()) {
191
+ throw new Error('not a safe directory');
192
+ }
193
+ }
194
+ catch (error) {
195
+ if (error.code === 'ENOENT')
196
+ return;
197
+ throw new Error('SmartDB storage-root relocation workspace inspection is ambiguous');
198
+ }
199
+ try {
200
+ await plugins.fs.lstat(workspacePath);
201
+ }
202
+ catch (error) {
203
+ if (error.code === 'ENOENT')
204
+ return;
205
+ throw new Error('SmartDB storage-root relocation workspace inspection is ambiguous');
206
+ }
207
+ throw new Error('SmartDB storage-root relocation recovery must complete before startup');
208
+ };
209
+ export const isLocalSmartDbStorageRootRelocationErrorCode = (valueArg) => typeof valueArg === 'string'
210
+ && localSmartDbStorageRootRelocationErrorCodes.includes(valueArg);
211
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmFnZS1yb290LXJlbG9jYXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy90c19zbWFydGRiL3N0b3JhZ2Utcm9vdC1yZWxvY2F0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBRXhDLE1BQU0sQ0FBQyxNQUFNLDJDQUEyQyxHQUFHO0lBQ3pELGlCQUFpQjtJQUNqQixhQUFhO0lBQ2IsTUFBTTtJQUNOLFdBQVc7SUFDWCxzQkFBc0I7SUFDdEIsbUJBQW1CO0lBQ25CLGVBQWU7SUFDZixJQUFJO0lBQ0osbUJBQW1CO0NBQ1gsQ0FBQztBQXdCWCxNQUFNLE9BQU8sc0NBQXVDLFNBQVEsS0FBSztJQUcvRCxZQUFtQixPQUFvRDtRQUNyRSxLQUFLLENBQUMsNERBQTRELE9BQU8sRUFBRSxDQUFDLENBQUM7UUFDN0UsSUFBSSxDQUFDLElBQUksR0FBRyx3Q0FBd0MsQ0FBQztRQUNyRCxJQUFJLENBQUMsSUFBSSxHQUFHLE9BQU8sQ0FBQztJQUN0QixDQUFDO0NBQ0Y7QUFFRCxNQUFNLG1CQUFtQixHQUFHO0lBQzFCLGtCQUFrQjtJQUNsQix1QkFBdUI7SUFDdkIsY0FBYztDQUNOLENBQUM7QUFDWCxNQUFNLHFCQUFxQixHQUFHO0lBQzVCLFFBQVE7SUFDUixTQUFTO0lBQ1QsY0FBYztJQUNkLGtCQUFrQjtJQUNsQix1QkFBdUI7SUFDdkIsZ0JBQWdCO0lBQ2hCLG1CQUFtQjtJQUNuQixrQkFBa0I7SUFDbEIsZUFBZTtJQUNmLHVCQUF1QjtDQUNmLENBQUM7QUFDWCxNQUFNLHNCQUFzQixHQUFHLElBQUksQ0FBQztBQUNwQyxNQUFNLHdCQUF3QixHQUFHLEdBQUcsQ0FBQztBQUNyQyxNQUFNLGlCQUFpQixHQUFHLHNCQUFzQixDQUFDO0FBQ2pELE1BQU0sbUJBQW1CLEdBQUcsOENBQThDLENBQUM7QUFFM0UsTUFBTSxlQUFlLEdBQUcsQ0FDdEIsT0FBb0Qs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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartdb",
3
- "version": "5.0.5",
3
+ "version": "5.1.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.hints.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # smartdb hints
2
2
 
3
+ ## Stopped Storage-Root Relocation
4
+
5
+ - `LocalSmartDb.relocateStoppedStorageRoot()` is the only supported path-changing operation. It uses a dedicated serialized mutating sidecar runner; do not route it through the disposable offline-inspection helper. A bridge whose termination is unconfirmed remains in the static cleanup gate, and no later relocation may spawn until cleanup succeeds.
6
+ - Rust owns the descriptor-relative operation in `rust/crates/rustdb/src/storage_root_relocation.rs`. It never constructs `RustDb`. The fixed journal phases are `preparing`, `prepared`, `exchanged`, `providerPublished`, `sentinelPublished`, and `cleaned`; retries reconcile the exact source-root/destination-absent-or-receipt topology or source-receipt/destination-root topology before continuing.
7
+ - The destination receipt is published with `RENAME_NOREPLACE`, then exchanged with the source root using `RENAME_EXCHANGE`. Provider and sentinel replacement also uses checked exchanges, retaining each previous object in the workspace until the replacement is durable and verified. The original root inode ends at the destination and the original receipt inode remains at the source indefinitely. Parent, root, internal, lock, provider, and sentinel identities are revalidated around publication.
8
+ - Eligibility is intentionally narrow: Linux, one supported local filesystem, exact current owner/provider/sentinel metadata, no auth path digest, and no resource receipt, allocation, retired lock, durable database/transaction workspace, database marker, symlink, unknown entry, or unsafe ownership/mode/link profile. Missing metadata is rejected and never initialized by relocation.
9
+ - Both TypeScript startup paths check for `.__rustdb_internal/storage-root-relocation` before `StorageMigrator`. Rust checks immediately after securely opening an existing root and repeats the descriptor-relative check after acquiring the owner lock, before storage initialization. This closes the interval where a relocation can create its workspace and release the lock after startup's first inspection. Occupancy or inspection ambiguity is a startup error; only an exact relocation retry performs recovery.
10
+ - Completed roots retain the SmartDB 5.0.1 provider/sentinel JSON shape, changing only their storage-root path digest. Interrupted state requires 5.1.0 or newer. Exact replay lasts only while the destination remains current; a later relocation supersedes the old source/destination tuple.
11
+
3
12
  ## Restricted Deny-All Views
4
13
 
5
14
  - `rustdb-commands/src/views.rs` is the sole owner of `system.views` parsing, bounds, canonical `_id_` metadata validation, exact deny-all pipeline validation, and authoritative namespace classification. There is intentionally no mutable cross-command view cache; aggregate and transaction commit reuse one immutable authoritative catalog per database for that operation.
package/readme.md CHANGED
@@ -539,9 +539,59 @@ const db = new LocalSmartDb({
539
539
  | `getConnectionUri()` | `string` | Get the connection URI |
540
540
  | `getServer()` | `SmartdbServer` | Access the underlying server |
541
541
  | `running` | `boolean` | Whether the server is running |
542
+ | `LocalSmartDb.relocateStoppedStorageRoot(input, options?)` | `Promise<ILocalSmartDbStoppedStorageRootRelocationReceipt>` | Atomically relocate one eligible stopped Linux file-storage root and retain a source receipt |
542
543
  | `LocalSmartDb.inspectOfflineStringValue(input, options?)` | `Promise<TLocalSmartDbOfflineStringValueInspectionResult>` | Read one exact top-level string value from stopped file storage without starting or mutating the engine |
543
544
  | `LocalSmartDb.inspectOfflinePhysicalNamespaces(input, options?)` | `Promise<ILocalSmartDbOfflinePhysicalNamespaceInspectionResult>` | Validate stopped physical file storage and return only deterministic database and collection names |
544
545
 
546
+ #### Stopped Storage-Root Relocation
547
+
548
+ `relocateStoppedStorageRoot()` moves an eligible stopped file-storage root without starting `RustDb`, binding a listener, running a TypeScript migration, initializing storage or auth, or performing WAL recovery. It is Linux-only and requires the source root, source parent, and destination parent to be trusted descriptor-safe objects on the same supported local filesystem. The supplied source and destination must be absolute and resolve to distinct, non-root, non-nested paths, and the destination must be absent. The trusted parents may differ.
549
+
550
+ ```typescript
551
+ import { LocalSmartDb } from '@push.rocks/smartdb';
552
+
553
+ const receipt = await LocalSmartDb.relocateStoppedStorageRoot({
554
+ sourceFolderPath: '/var/lib/myapp/harness-controller/smartdb',
555
+ destinationFolderPath: '/var/lib/myapp/hcon/smartdb',
556
+ relocationId: 'move-2026-08-17-1',
557
+ }, {
558
+ timeoutMs: 30_000,
559
+ });
560
+
561
+ // Start only the destination. The source is now the retained receipt file.
562
+ const db = new LocalSmartDb({ folderPath: receipt.destinationFolderPath });
563
+ await db.start();
564
+ ```
565
+
566
+ The input must be a plain object with exactly the own enumerable data fields `sourceFolderPath`, `destinationFolderPath`, and `relocationId`. Each supplied path must already be absolute, is normalized before sidecar creation, is limited to 1 through 4095 UTF-8 bytes, and cannot contain control characters. The normalized destination path may not be longer in UTF-8 bytes than the normalized source path, so every root-relative path accepted by SmartDB 5.0.1 at the source remains no longer after relocation. `relocationId` is a 1 through 256-byte ASCII identifier beginning with an alphanumeric character and otherwise using only alphanumerics, `.`, `_`, `:`, `@`, or `-`. Management `timeoutMs` and `signal` use the same validation and one-deadline behavior as other SmartDB management operations.
567
+
568
+ The returned exact receipt has this shape:
569
+
570
+ ```typescript
571
+ interface ILocalSmartDbStoppedStorageRootRelocationReceipt {
572
+ format: 'smartdb.storage-root-relocation.receipt.v1';
573
+ version: 1;
574
+ relocationId: string;
575
+ sourceFolderPath: string;
576
+ destinationFolderPath: string;
577
+ providerRootId: string;
578
+ storageRootDevice: string; // canonical decimal u64
579
+ storageRootInode: string; // canonical decimal u64
580
+ receiptSha256: string;
581
+ sourceReceiptRetained: true;
582
+ }
583
+ ```
584
+
585
+ On success, the destination directory is the original storage-root inode and the source path is an exact mode-0600 regular-file receipt. SmartDB does not remove that source receipt. Data, WAL, hint, and index bytes are not copied or rewritten. `providerRootId`, root device/inode, and every provider/sentinel field are preserved; only `storageRootSha256` changes to the destination's canonical path digest. This leaves a completed destination compatible with SmartDB 5.0.1.
586
+
587
+ Relocation deliberately supports only the plain, unbound `LocalSmartDb` topology. The existing owner lock, `provider-identity.json`, and resource-fencing sentinel must all be present; the operation never creates missing ownership metadata. A provider identity containing `authUsersPathSha256` is unsupported. Any durable fence receipt or capability, allocation state, retired fence lock, database mutation/transaction journal, allocation/publication marker, symlink, unknown entry, unsafe mode/owner/link count, exhausted scan bound, active owner, or ambiguous identity fails closed.
588
+
589
+ The operation journals phases `preparing`, `prepared`, `exchanged`, `providerPublished`, `sentinelPublished`, and `cleaned`. If a call returns `RECOVERY_REQUIRED`, stop all startup attempts and retry the exact same normalized source path, destination path, and `relocationId` with SmartDB 5.1.0 or newer. Startup rejects any relocation-workspace occupancy and does not mutate or recover it. Do not delete, rename, edit, or replace either path between attempts. An interrupted relocation requires 5.1.0-or-newer recovery even though a completed root remains 5.0.1-compatible.
590
+
591
+ An exact completed replay returns the same receipt only while the destination remains the current root. A later successful relocation leaves another receipt at that destination path and supersedes the earlier replay topology; retry the later operation instead. Manual copy or rename remains unsupported and continues to fail provider path/inode fencing.
592
+
593
+ Failures are exposed as `LocalSmartDbStorageRootRelocationError` with a code-only union: `INVALID_REQUEST`, `UNSUPPORTED`, `BUSY`, `NOT_FOUND`, `DESTINATION_CONFLICT`, `IDENTITY_MISMATCH`, `STATE_CORRUPT`, `IO`, or `RECOVERY_REQUIRED`. Error messages and causes do not include supplied paths. Timeout, abort, write, exit, or malformed-response failures after command dispatch are always `RECOVERY_REQUIRED`; exact retry is the only recovery action.
594
+
545
595
  #### Offline String-Value Inspection
546
596
 
547
597
  `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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdb',
6
- version: '5.0.5',
6
+ version: '5.1.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
@@ -14,6 +14,13 @@ export type {
14
14
  ILocalSmartDbOfflinePhysicalNamespaceInspectionResult,
15
15
  ILocalSmartDbOfflineStringValueInspection,
16
16
  TLocalSmartDbOfflineStringValueInspectionResult,
17
+ ILocalSmartDbStoppedStorageRootRelocationInput,
18
+ ILocalSmartDbStoppedStorageRootRelocationReceipt,
19
+ TLocalSmartDbStorageRootRelocationErrorCode,
20
+ } from './ts_local/index.js';
21
+ export {
22
+ LocalSmartDbStorageRootRelocationError,
23
+ localSmartDbStorageRootRelocationErrorCodes,
17
24
  } from './ts_local/index.js';
18
25
 
19
26
  // Export migration
@@ -4,20 +4,32 @@ import * as net from 'net';
4
4
  import * as path from 'path';
5
5
  import * as os from 'os';
6
6
  import {
7
- RustDbBridge,
8
7
  SmartdbServer,
9
8
  type ILocalSmartDbOfflinePhysicalNamespaceInspection,
10
9
  type ILocalSmartDbOfflinePhysicalNamespaceInspectionResult,
10
+ type ILocalSmartDbStoppedStorageRootRelocationInput,
11
+ type ILocalSmartDbStoppedStorageRootRelocationReceipt,
11
12
  type ILocalSmartDbOfflineStringValueInspection,
12
13
  type ISmartDbManagementOperationOptions,
13
14
  type TLocalSmartDbOfflineStringValueInspectionResult,
14
15
  } from '../ts_smartdb/index.js';
16
+ import {
17
+ RustDbBridge,
18
+ rustDbBridgeStorageRootRelocation,
19
+ } from '../ts_smartdb/rust-db-bridge.js';
15
20
  import { StorageMigrator } from '../ts_migration/index.js';
16
21
  import {
17
22
  normalizeLocalSmartDbOfflinePhysicalNamespaceInspectionInput,
18
23
  normalizeLocalSmartDbOfflineInspectionInput,
19
24
  validateSmartDbManagementOperationOptions,
20
25
  } from '../ts_smartdb/offline-inspection.js';
26
+ import {
27
+ assertNoLocalSmartDbStorageRootRelocationWorkspace,
28
+ isLocalSmartDbStorageRootRelocationErrorCode,
29
+ LocalSmartDbStorageRootRelocationError,
30
+ normalizeLocalSmartDbStoppedStorageRootRelocationInput,
31
+ normalizeLocalSmartDbStoppedStorageRootRelocationReceipt,
32
+ } from '../ts_smartdb/storage-root-relocation.js';
21
33
 
22
34
  /**
23
35
  * Connection information returned by LocalSmartDb.start()
@@ -65,6 +77,8 @@ export interface ILocalSmartDbOptions {
65
77
  * ```
66
78
  */
67
79
  export class LocalSmartDb {
80
+ private static storageRootRelocationGate: Promise<void> = Promise.resolve();
81
+ private static storageRootRelocationBridge: RustDbBridge | undefined;
68
82
  private options: ILocalSmartDbOptions;
69
83
  private server: SmartdbServer | null = null;
70
84
  private generatedSocketPath: string | null = null;
@@ -77,6 +91,141 @@ export class LocalSmartDb {
77
91
  return new RustDbBridge();
78
92
  }
79
93
 
94
+ private static createStorageRootRelocationBridge(): RustDbBridge {
95
+ return new RustDbBridge();
96
+ }
97
+
98
+ private static async withStorageRootRelocationGate<TResult>(
99
+ operationArg: () => Promise<TResult>,
100
+ ): Promise<TResult> {
101
+ const previous = LocalSmartDb.storageRootRelocationGate;
102
+ let release!: () => void;
103
+ LocalSmartDb.storageRootRelocationGate = new Promise<void>((resolveArg) => {
104
+ release = resolveArg;
105
+ });
106
+ await previous;
107
+ try {
108
+ return await operationArg();
109
+ } finally {
110
+ release();
111
+ }
112
+ }
113
+
114
+ private static remainingManagementTimeout(
115
+ deadlineArg: number | undefined,
116
+ ): number | undefined {
117
+ if (deadlineArg === undefined) return undefined;
118
+ const remaining = deadlineArg - Date.now();
119
+ if (remaining <= 0) {
120
+ throw new LocalSmartDbStorageRootRelocationError('IO');
121
+ }
122
+ return remaining;
123
+ }
124
+
125
+ /**
126
+ * Relocate one stopped, plain file-storage root without starting its engine.
127
+ */
128
+ public static async relocateStoppedStorageRoot(
129
+ inputArg: ILocalSmartDbStoppedStorageRootRelocationInput,
130
+ optionsArg?: ISmartDbManagementOperationOptions,
131
+ ): Promise<ILocalSmartDbStoppedStorageRootRelocationReceipt> {
132
+ let input: ILocalSmartDbStoppedStorageRootRelocationInput;
133
+ try {
134
+ input = normalizeLocalSmartDbStoppedStorageRootRelocationInput(inputArg);
135
+ validateSmartDbManagementOperationOptions(optionsArg);
136
+ } catch (error) {
137
+ if (error instanceof LocalSmartDbStorageRootRelocationError) throw error;
138
+ throw new LocalSmartDbStorageRootRelocationError('INVALID_REQUEST');
139
+ }
140
+ if (optionsArg?.signal?.aborted) {
141
+ throw new LocalSmartDbStorageRootRelocationError('IO');
142
+ }
143
+ if (process.platform !== 'linux') {
144
+ throw new LocalSmartDbStorageRootRelocationError('UNSUPPORTED');
145
+ }
146
+ const deadline = optionsArg?.timeoutMs === undefined
147
+ ? undefined
148
+ : Date.now() + optionsArg.timeoutMs;
149
+
150
+ return LocalSmartDb.withStorageRootRelocationGate(async () => {
151
+ const retainedBridge = LocalSmartDb.storageRootRelocationBridge;
152
+ if (retainedBridge) {
153
+ try {
154
+ await retainedBridge.terminate({ gracePeriodMs: 0 });
155
+ } catch {
156
+ throw new LocalSmartDbStorageRootRelocationError('RECOVERY_REQUIRED');
157
+ }
158
+ if (!retainedBridge.ownershipReleased) {
159
+ throw new LocalSmartDbStorageRootRelocationError('RECOVERY_REQUIRED');
160
+ }
161
+ if (LocalSmartDb.storageRootRelocationBridge === retainedBridge) {
162
+ LocalSmartDb.storageRootRelocationBridge = undefined;
163
+ }
164
+ }
165
+
166
+ let bridge: RustDbBridge | undefined;
167
+ let receipt: ILocalSmartDbStoppedStorageRootRelocationReceipt | undefined;
168
+ let operationError: LocalSmartDbStorageRootRelocationError | undefined;
169
+ try {
170
+ bridge = LocalSmartDb.createStorageRootRelocationBridge();
171
+ LocalSmartDb.storageRootRelocationBridge = bridge;
172
+ const spawnTimeout = LocalSmartDb.remainingManagementTimeout(deadline);
173
+ const spawned = await bridge.spawn({
174
+ ...(optionsArg?.signal ? { signal: optionsArg.signal } : {}),
175
+ ...(spawnTimeout === undefined ? {} : { timeoutMs: spawnTimeout }),
176
+ });
177
+ if (!spawned) {
178
+ throw new LocalSmartDbStorageRootRelocationError('IO');
179
+ }
180
+ const commandTimeout = LocalSmartDb.remainingManagementTimeout(deadline);
181
+ const result = await bridge[rustDbBridgeStorageRootRelocation](input, {
182
+ ...(optionsArg?.signal ? { signal: optionsArg.signal } : {}),
183
+ ...(commandTimeout === undefined ? {} : { timeoutMs: commandTimeout }),
184
+ });
185
+ receipt = normalizeLocalSmartDbStoppedStorageRootRelocationReceipt(result, input);
186
+ } catch (error) {
187
+ if (error instanceof LocalSmartDbStorageRootRelocationError) {
188
+ operationError = error;
189
+ } else {
190
+ const requestWasDispatched = error instanceof Error
191
+ && 'requestId' in error
192
+ && typeof error.requestId === 'string';
193
+ if (
194
+ requestWasDispatched
195
+ && error instanceof Error
196
+ && 'responseErrorCode' in error
197
+ && isLocalSmartDbStorageRootRelocationErrorCode(error.responseErrorCode)
198
+ ) {
199
+ operationError = new LocalSmartDbStorageRootRelocationError(error.responseErrorCode);
200
+ } else {
201
+ operationError = new LocalSmartDbStorageRootRelocationError(
202
+ requestWasDispatched ? 'RECOVERY_REQUIRED' : 'IO',
203
+ );
204
+ }
205
+ }
206
+ }
207
+
208
+ if (bridge) {
209
+ try {
210
+ await bridge.terminate({ gracePeriodMs: 0 });
211
+ if (!bridge.ownershipReleased) {
212
+ throw new Error('termination was not confirmed');
213
+ }
214
+ if (LocalSmartDb.storageRootRelocationBridge === bridge) {
215
+ LocalSmartDb.storageRootRelocationBridge = undefined;
216
+ }
217
+ } catch {
218
+ operationError = new LocalSmartDbStorageRootRelocationError('RECOVERY_REQUIRED');
219
+ }
220
+ }
221
+ if (operationError) throw operationError;
222
+ if (!receipt) {
223
+ throw new LocalSmartDbStorageRootRelocationError('RECOVERY_REQUIRED');
224
+ }
225
+ return receipt;
226
+ });
227
+ }
228
+
80
229
  private static async runOfflineInspection<
81
230
  TInput extends { folderPath: string },
82
231
  TResult,
@@ -261,6 +410,7 @@ export class LocalSmartDb {
261
410
  await LocalSmartDb.cleanStaleSockets();
262
411
 
263
412
  // Run storage migration before starting the Rust engine
413
+ await assertNoLocalSmartDbStorageRootRelocationWorkspace(this.options.folderPath);
264
414
  const migrator = new StorageMigrator(this.options.folderPath);
265
415
  await migrator.run();
266
416
 
@@ -8,3 +8,12 @@ export type {
8
8
  ILocalSmartDbOfflineStringValueInspection,
9
9
  TLocalSmartDbOfflineStringValueInspectionResult,
10
10
  } from '../ts_smartdb/service-types.js';
11
+ export type {
12
+ ILocalSmartDbStoppedStorageRootRelocationInput,
13
+ ILocalSmartDbStoppedStorageRootRelocationReceipt,
14
+ TLocalSmartDbStorageRootRelocationErrorCode,
15
+ } from '../ts_smartdb/storage-root-relocation.js';
16
+ export {
17
+ LocalSmartDbStorageRootRelocationError,
18
+ localSmartDbStorageRootRelocationErrorCodes,
19
+ } from '../ts_smartdb/storage-root-relocation.js';
@@ -81,5 +81,14 @@ export {
81
81
  normalizeSmartDbResourceFenceError,
82
82
  smartDbResourceFenceErrorCodes,
83
83
  } from './service-types.js';
84
+ export type {
85
+ ILocalSmartDbStoppedStorageRootRelocationInput,
86
+ ILocalSmartDbStoppedStorageRootRelocationReceipt,
87
+ TLocalSmartDbStorageRootRelocationErrorCode,
88
+ } from './storage-root-relocation.js';
89
+ export {
90
+ LocalSmartDbStorageRootRelocationError,
91
+ localSmartDbStorageRootRelocationErrorCodes,
92
+ } from './storage-root-relocation.js';
84
93
  export type { TSmartDbEffectivePayloadInputV1 } from './resource-fencing.js';
85
94
  export { createSmartDbEffectivePayloadSha256V1 } from './resource-fencing.js';
@@ -1,8 +1,10 @@
1
1
  // node native scope
2
2
  import * as buffer from 'node:buffer';
3
3
  import * as crypto from 'node:crypto';
4
+ import * as fs from 'node:fs/promises';
5
+ import * as path from 'node:path';
4
6
 
5
- export { buffer, crypto };
7
+ export { buffer, crypto, fs, path };
6
8
 
7
9
  // @push.rocks scope
8
10
  import * as smartrust from '@push.rocks/smartrust';
@@ -11,6 +11,11 @@ import {
11
11
  normalizeLocalSmartDbOfflineInspectionInput,
12
12
  validateSmartDbManagementOperationOptions,
13
13
  } from './offline-inspection.js';
14
+ import {
15
+ normalizeLocalSmartDbStoppedStorageRootRelocationInput,
16
+ type ILocalSmartDbStoppedStorageRootRelocationInput,
17
+ type ILocalSmartDbStoppedStorageRootRelocationReceipt,
18
+ } from './storage-root-relocation.js';
14
19
  import type {
15
20
  ISmartDbHealth,
16
21
  ISmartDbDatabaseTenantInput,
@@ -180,6 +185,10 @@ type TOfflinePhysicalNamespaceInspectionCommandParams = {
180
185
  } & ILocalSmartDbOfflinePhysicalNamespaceInspection['limits'];
181
186
 
182
187
  type TSmartDbCommands = {
188
+ relocateStoppedStorageRoot: {
189
+ params: ILocalSmartDbStoppedStorageRootRelocationInput;
190
+ result: ILocalSmartDbStoppedStorageRootRelocationReceipt;
191
+ };
183
192
  inspectOfflinePhysicalNamespaces: {
184
193
  params: TOfflinePhysicalNamespaceInspectionCommandParams;
185
194
  result: ILocalSmartDbOfflinePhysicalNamespaceInspectionResult;
@@ -276,6 +285,10 @@ type TSmartDbCommands = {
276
285
  };
277
286
  };
278
287
 
288
+ export const rustDbBridgeStorageRootRelocation = Symbol(
289
+ 'smartdb.internal.storage-root-relocation',
290
+ );
291
+
279
292
  export type TAuthMetadataPermissionTransitionStatus =
280
293
  | 'not-present'
281
294
  | 'already-target'
@@ -557,6 +570,19 @@ export class RustDbBridge extends EventEmitter {
557
570
 
558
571
  // --- Convenience methods for each management command ---
559
572
 
573
+ public async [rustDbBridgeStorageRootRelocation](
574
+ inputArg: ILocalSmartDbStoppedStorageRootRelocationInput,
575
+ optionsArg?: ISmartDbManagementOperationOptions,
576
+ ): Promise<ILocalSmartDbStoppedStorageRootRelocationReceipt> {
577
+ const input = normalizeLocalSmartDbStoppedStorageRootRelocationInput(inputArg);
578
+ validateSmartDbManagementOperationOptions(optionsArg);
579
+ return await this.bridge.sendCommand(
580
+ 'relocateStoppedStorageRoot',
581
+ input,
582
+ optionsArg,
583
+ ) as ILocalSmartDbStoppedStorageRootRelocationReceipt;
584
+ }
585
+
560
586
  public async inspectOfflinePhysicalNamespaces(
561
587
  inputArg: ILocalSmartDbOfflinePhysicalNamespaceInspection,
562
588
  optionsArg?: ISmartDbManagementOperationOptions,