@push.rocks/smartdb 5.0.4 → 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.4",
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
@@ -354,7 +354,7 @@ Legacy v0 JSON collections are converted into hidden sibling staging directories
354
354
 
355
355
  Persisted users also carry a random principal identity and a monotonic generation. SmartDB reloads and resolves that identity for every authenticated command, so password or role changes take effect immediately and stale sockets are rejected. Deleting and recreating the same username creates a different principal; an old connection cannot inherit the replacement user's authority. Cross-process user updates are serialized through the persisted users-file lock.
356
356
 
357
- Single-node transactions are supported through official MongoDB driver sessions. Writes with `startTransaction` and `autocommit: false` are buffered per logical session, reads inside the transaction see the buffered overlay, `commitTransaction` applies the write set with conflict checks, and `abortTransaction` discards it. Live logical sessions remain resumable across socket disconnects. Bounded background cleanup aborts expired transactions, removes expired sessions, and releases publication leases; explicit `endSessions` and `killSessions` do the same for their active transactions.
357
+ Single-node transactions are supported through official MongoDB driver sessions. `find`, `count`, `distinct`, `insert`, `update`, `findAndModify`, and `delete` use the transaction snapshot and buffered write set; `commitTransaction` applies that write set with conflict checks, and `abortTransaction` discards it. After applicable authentication, authorization, and allocation-policy checks, collection, database, and index DDL, user-management mutations, and aggregate pipelines ending in `$out` or `$merge` reject transaction envelopes with `OperationNotSupportedInTransaction` (code 263) before session or transaction creation, maintenance gates, or mutation. Live logical sessions remain resumable across socket disconnects. Bounded background cleanup aborts expired transactions, removes expired sessions, and releases publication leases; explicit `endSessions` and `killSessions` do the same for their active transactions.
358
358
 
359
359
  ### Durable Database Publication Holds
360
360
 
@@ -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.4',
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';