@push.rocks/smartdb 2.18.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_rust/rustdb_linux_amd64 +0 -0
- package/dist_rust/rustdb_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/index.js +1 -1
- package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.d.ts +2 -1
- package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.js +3 -3
- package/dist_ts/ts_migration/classes.storagemigrator.d.ts +2 -1
- package/dist_ts/ts_migration/classes.storagemigrator.js +36 -20
- package/dist_ts/ts_migration/index.d.ts +1 -0
- package/dist_ts/ts_migration/index.js +2 -1
- package/dist_ts/ts_migration/migrators/v0_to_v1.d.ts +7 -1
- package/dist_ts/ts_migration/migrators/v0_to_v1.js +162 -47
- package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.d.ts +3 -2
- package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.js +3 -3
- package/dist_ts/ts_smartdb/index.d.ts +1 -1
- package/dist_ts/ts_smartdb/index.js +1 -1
- package/dist_ts/ts_smartdb/resource-fencing.d.ts +12 -1
- package/dist_ts/ts_smartdb/resource-fencing.js +44 -2
- package/dist_ts/ts_smartdb/rust-db-bridge.d.ts +5 -4
- package/dist_ts/ts_smartdb/rust-db-bridge.js +33 -10
- package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +4 -2
- package/dist_ts/ts_smartdb/server/SmartdbServer.js +84 -9
- package/dist_ts/ts_smartdb/service-types.d.ts +47 -2
- package/dist_ts/ts_smartdb/service-types.js +7 -2
- package/package.json +1 -1
- package/readme.md +19 -4
- package/readme.plan.md +2 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +4 -0
- package/ts/ts_migration/classes.authmetadatamigrationrunner.ts +3 -1
- package/ts/ts_migration/classes.storagemigrator.ts +38 -19
- package/ts/ts_migration/index.ts +1 -0
- package/ts/ts_migration/migrators/v0_to_v1.ts +224 -44
- package/ts/ts_migration/migrators/v1_auth_metadata_permissions.ts +4 -0
- package/ts/ts_smartdb/index.ts +4 -0
- package/ts/ts_smartdb/resource-fencing.ts +59 -2
- package/ts/ts_smartdb/rust-db-bridge.ts +56 -11
- package/ts/ts_smartdb/server/SmartdbServer.ts +105 -9
- package/ts/ts_smartdb/service-types.ts +59 -2
|
@@ -5,6 +5,9 @@ export interface ISmartDbHealth {
|
|
|
5
5
|
resourceFencingVersion?: 1;
|
|
6
6
|
resourceFencingSupported?: boolean;
|
|
7
7
|
resourceFencingRequiresDrain?: true;
|
|
8
|
+
allocationFencingVersion?: 1;
|
|
9
|
+
allocationFencingSupported?: boolean;
|
|
10
|
+
allocationFencingRequiresDrain?: true;
|
|
8
11
|
publicationHoldVersion: 1;
|
|
9
12
|
publicationHoldSupported: boolean;
|
|
10
13
|
publicationHoldRequiresExternalDrain: boolean;
|
|
@@ -39,7 +42,7 @@ export type TLocalSmartDbOfflineStringValueInspectionResult = {
|
|
|
39
42
|
status: 'found';
|
|
40
43
|
value: string;
|
|
41
44
|
};
|
|
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"];
|
|
45
|
+
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", "EALLOCATION_EXPECTED_ABSENT_CONFLICT", "EALLOCATION_REQUIRED", "EALLOCATION_MISMATCH", "EALLOCATION_GENERATION_EXHAUSTED", "EALLOCATION_DEPROVISIONED"];
|
|
43
46
|
export type TSmartDbResourceFenceErrorCode = (typeof smartDbResourceFenceErrorCodes)[number];
|
|
44
47
|
export interface ISmartDbResourceFence {
|
|
45
48
|
version: 1;
|
|
@@ -49,7 +52,7 @@ export interface ISmartDbResourceFence {
|
|
|
49
52
|
payloadSha256: string;
|
|
50
53
|
}
|
|
51
54
|
export interface ISmartDbResourceFenceReceipt extends ISmartDbResourceFence {
|
|
52
|
-
kind: 'smartdb.database.replace.v1' | 'smartdb.database.delete.v1' | 'smartdb.database.ensure.v1';
|
|
55
|
+
kind: 'smartdb.database.replace.v1' | 'smartdb.database.delete.v1' | 'smartdb.database.ensure.v1' | 'smartdb.database.allocate.v1';
|
|
53
56
|
/** Digest of the canonical payload interpretation derived by SmartDB. */
|
|
54
57
|
effectivePayloadSha256: string;
|
|
55
58
|
provider?: 'smartdb';
|
|
@@ -62,6 +65,33 @@ export interface ISmartDbResourceFenceReceipt extends ISmartDbResourceFence {
|
|
|
62
65
|
publicationCapabilitySha256?: string;
|
|
63
66
|
/** Present only while publication is held; never returned after release. */
|
|
64
67
|
publicationCapability?: string;
|
|
68
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
69
|
+
}
|
|
70
|
+
export interface ISmartDbDatabaseAllocationIdentity {
|
|
71
|
+
version: 1;
|
|
72
|
+
provider: 'smartdb';
|
|
73
|
+
state: 'active';
|
|
74
|
+
databaseName: string;
|
|
75
|
+
username: string;
|
|
76
|
+
allocationId: string;
|
|
77
|
+
generation: number;
|
|
78
|
+
principalId: string;
|
|
79
|
+
resourceIdSha256: string;
|
|
80
|
+
bindingSha256: string;
|
|
81
|
+
receiptSha256: string;
|
|
82
|
+
}
|
|
83
|
+
export interface ISmartDbDatabaseAllocationLifecycle {
|
|
84
|
+
version: 1;
|
|
85
|
+
provider: 'smartdb';
|
|
86
|
+
state: 'allocating' | 'active' | 'deprovisioning' | 'deprovisioned';
|
|
87
|
+
databaseName: string;
|
|
88
|
+
username: string;
|
|
89
|
+
allocationId: string;
|
|
90
|
+
generation: number;
|
|
91
|
+
principalId: string;
|
|
92
|
+
resourceIdSha256: string;
|
|
93
|
+
bindingSha256: string;
|
|
94
|
+
receiptSha256: string;
|
|
65
95
|
}
|
|
66
96
|
export interface ISmartDbHeldPublicationReceipt extends ISmartDbResourceFenceReceipt {
|
|
67
97
|
provider: 'smartdb';
|
|
@@ -94,6 +124,7 @@ export interface ISmartDbDatabaseResourceFenceState {
|
|
|
94
124
|
scopeId: string;
|
|
95
125
|
highestToken: number;
|
|
96
126
|
publicationState: 'requested' | 'publishing' | 'held' | 'released';
|
|
127
|
+
allocation?: ISmartDbDatabaseAllocationLifecycle;
|
|
97
128
|
}
|
|
98
129
|
export declare class SmartDbResourceFenceError extends Error {
|
|
99
130
|
readonly code: TSmartDbResourceFenceErrorCode;
|
|
@@ -108,6 +139,11 @@ export interface ISmartDbDatabaseTenantInput {
|
|
|
108
139
|
}
|
|
109
140
|
export interface ISmartDbEnsureDatabaseTenantInput extends ISmartDbDatabaseTenantInput {
|
|
110
141
|
fence: ISmartDbResourceFence;
|
|
142
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
143
|
+
}
|
|
144
|
+
export interface ISmartDbAllocateDatabaseTenantInput extends ISmartDbDatabaseTenantInput {
|
|
145
|
+
expectedAbsent: true;
|
|
146
|
+
fence: ISmartDbResourceFence;
|
|
111
147
|
}
|
|
112
148
|
export interface ISmartDbDeleteDatabaseTenantInput {
|
|
113
149
|
databaseName: string;
|
|
@@ -121,6 +157,7 @@ export interface ISmartDbDeleteDatabaseTenantInput {
|
|
|
121
157
|
*/
|
|
122
158
|
username?: string;
|
|
123
159
|
fence?: ISmartDbResourceFence;
|
|
160
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
124
161
|
/** Keep the database data plane closed until commitDatabasePublication. */
|
|
125
162
|
holdPublication?: boolean;
|
|
126
163
|
}
|
|
@@ -135,10 +172,15 @@ export interface ISmartDbDatabaseTenantDescriptor {
|
|
|
135
172
|
authSource: string;
|
|
136
173
|
mongodbUri?: string;
|
|
137
174
|
resourceFence?: ISmartDbResourceFenceReceipt;
|
|
175
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
138
176
|
}
|
|
139
177
|
export interface ISmartDbEnsureDatabaseTenantResult extends ISmartDbDatabaseTenantDescriptor {
|
|
140
178
|
resourceFence: ISmartDbResourceFenceReceipt;
|
|
141
179
|
}
|
|
180
|
+
export interface ISmartDbAllocateDatabaseTenantResult extends ISmartDbDatabaseTenantDescriptor {
|
|
181
|
+
resourceFence: ISmartDbResourceFenceReceipt;
|
|
182
|
+
allocation: ISmartDbDatabaseAllocationIdentity;
|
|
183
|
+
}
|
|
142
184
|
export interface ISmartDbDeleteDatabaseTenantResult {
|
|
143
185
|
databaseName: string;
|
|
144
186
|
/**
|
|
@@ -154,6 +196,7 @@ export interface ISmartDbDeleteDatabaseTenantResult {
|
|
|
154
196
|
*/
|
|
155
197
|
usersPreserved?: boolean;
|
|
156
198
|
resourceFence?: ISmartDbResourceFenceReceipt;
|
|
199
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
157
200
|
}
|
|
158
201
|
export interface ISmartDbDatabaseExportCollection {
|
|
159
202
|
name: string;
|
|
@@ -206,6 +249,7 @@ export interface ISmartDbImportDatabaseInput {
|
|
|
206
249
|
*/
|
|
207
250
|
username?: string;
|
|
208
251
|
fence?: ISmartDbResourceFence;
|
|
252
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
209
253
|
/** Keep the database data plane closed until commitDatabasePublication. */
|
|
210
254
|
holdPublication?: boolean;
|
|
211
255
|
}
|
|
@@ -214,5 +258,6 @@ export interface ISmartDbImportDatabaseResult {
|
|
|
214
258
|
collections: number;
|
|
215
259
|
documents: number;
|
|
216
260
|
resourceFence?: ISmartDbResourceFenceReceipt;
|
|
261
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
217
262
|
}
|
|
218
263
|
export type TSmartDbCommitDatabasePublicationResult = ISmartDbImportDatabaseResult | ISmartDbDeleteDatabaseTenantResult;
|
|
@@ -11,6 +11,11 @@ export const smartDbResourceFenceErrorCodes = [
|
|
|
11
11
|
'EFENCE_PUBLICATION_HELD',
|
|
12
12
|
'EFENCE_CAPABILITY_MISMATCH',
|
|
13
13
|
'EFENCE_NOT_HELD',
|
|
14
|
+
'EALLOCATION_EXPECTED_ABSENT_CONFLICT',
|
|
15
|
+
'EALLOCATION_REQUIRED',
|
|
16
|
+
'EALLOCATION_MISMATCH',
|
|
17
|
+
'EALLOCATION_GENERATION_EXHAUSTED',
|
|
18
|
+
'EALLOCATION_DEPROVISIONED',
|
|
14
19
|
];
|
|
15
20
|
export class SmartDbResourceFenceError extends Error {
|
|
16
21
|
constructor(codeArg, messageArg, causeArg) {
|
|
@@ -23,11 +28,11 @@ export const normalizeSmartDbResourceFenceError = (errorArg) => {
|
|
|
23
28
|
const error = errorArg instanceof Error
|
|
24
29
|
? errorArg
|
|
25
30
|
: new Error(String(errorArg));
|
|
26
|
-
const match = /^(
|
|
31
|
+
const match = /^((?:EFENCE|EALLOCATION)_[A-Z_]+):\s*(.*)$/s.exec(error.message);
|
|
27
32
|
if (!match ||
|
|
28
33
|
!smartDbResourceFenceErrorCodes.includes(match[1])) {
|
|
29
34
|
return error;
|
|
30
35
|
}
|
|
31
36
|
return new SmartDbResourceFenceError(match[1], match[2], error);
|
|
32
37
|
};
|
|
33
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
38
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS10eXBlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3RzX3NtYXJ0ZGIvc2VydmljZS10eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUE2Q0EsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUc7SUFDNUMsaUJBQWlCO0lBQ2pCLGNBQWM7SUFDZCx1QkFBdUI7SUFDdkIsb0JBQW9CO0lBQ3BCLDBCQUEwQjtJQUMxQiwyQkFBMkI7SUFDM0Isc0JBQXNCO0lBQ3RCLHNCQUFzQjtJQUN0QixhQUFhO0lBQ2IseUJBQXlCO0lBQ3pCLDRCQUE0QjtJQUM1QixpQkFBaUI7SUFDakIsc0NBQXNDO0lBQ3RDLHNCQUFzQjtJQUN0QixzQkFBc0I7SUFDdEIsa0NBQWtDO0lBQ2xDLDJCQUEyQjtDQUNuQixDQUFDO0FBcUdYLE1BQU0sT0FBTyx5QkFBMEIsU0FBUSxLQUFLO0lBR2xELFlBQ0UsT0FBdUMsRUFDdkMsVUFBa0IsRUFDbEIsUUFBa0I7UUFFbEIsS0FBSyxDQUFDLEdBQUcsT0FBTyxLQUFLLFVBQVUsRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7UUFDeEQsSUFBSSxDQUFDLElBQUksR0FBRywyQkFBMkIsQ0FBQztRQUN4QyxJQUFJLENBQUMsSUFBSSxHQUFHLE9BQU8sQ0FBQztJQUN0QixDQUFDO0NBQ0Y7QUFFRCxNQUFNLENBQUMsTUFBTSxrQ0FBa0MsR0FBRyxDQUNoRCxRQUFpQixFQUNWLEVBQUU7SUFDVCxNQUFNLEtBQUssR0FDVCxRQUFRLFlBQVksS0FBSztRQUN2QixDQUFDLENBQUMsUUFBUTtRQUNWLENBQUMsQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQztJQUNsQyxNQUFNLEtBQUssR0FBRyw2Q0FBNkMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ2hGLElBQ0UsQ0FBQyxLQUFLO1FBQ04sQ0FBQyw4QkFBOEIsQ0FBQyxRQUFRLENBQ3RDLEtBQUssQ0FBQyxDQUFDLENBQW1DLENBQzNDLEVBQ0QsQ0FBQztRQUNELE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUNELE9BQU8sSUFBSSx5QkFBeUIsQ0FDbEMsS0FBSyxDQUFDLENBQUMsQ0FBbUMsRUFDMUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUNSLEtBQUssQ0FDTixDQUFDO0FBQ0osQ0FBQyxDQUFDIn0=
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -272,7 +272,11 @@ const server = new SmartdbServer({
|
|
|
272
272
|
storage: 'memory', // 'memory' or 'file' (default: 'memory')
|
|
273
273
|
storagePath: './data', // Required when storage is 'file'
|
|
274
274
|
});
|
|
275
|
-
|
|
275
|
+
const startupController = new AbortController();
|
|
276
|
+
await server.start({
|
|
277
|
+
signal: startupController.signal,
|
|
278
|
+
timeoutMs: 30_000,
|
|
279
|
+
});
|
|
276
280
|
console.log(server.port); // Actual bound port while running
|
|
277
281
|
console.log(server.getConnectionUri()); // mongodb://127.0.0.1:<actual-port>
|
|
278
282
|
|
|
@@ -346,6 +350,8 @@ Supported built-in role names are `root`, `read`, `readWrite`, `dbAdmin`, `userA
|
|
|
346
350
|
|
|
347
351
|
On Linux, startup performs one narrow compatibility migration before the Rust engine opens `usersPath`: an effective-user-owned, single-link regular file with the exact legacy mode `0644` is changed to `0600`. Current `0600` files are left unchanged. Symlinks, hard links, unexpected modes or ownership, unsafe parent directories, cross-device targets, and identity changes remain fail-closed errors.
|
|
348
352
|
|
|
353
|
+
Legacy v0 JSON collections are converted into hidden sibling staging directories. SmartDB fsyncs every generated file and the staging directory before atomically publishing a complete v1 collection, while the v0 files remain unchanged. Cancellation removes unpublished staging and preserves the v0 input. Startup fails closed if it finds staging left by an unclean process exit or an incomplete published target, so neither state is mistaken for a complete migration.
|
|
354
|
+
|
|
349
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.
|
|
350
356
|
|
|
351
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.
|
|
@@ -394,13 +400,21 @@ const released = await server.commitDatabasePublication({
|
|
|
394
400
|
|
|
395
401
|
The held barrier survives restart and blocks wire commands, transactions, startup recovery, compaction, index restoration, and close-time hint writes for that database while unrelated databases remain available. Commit is exact and idempotent. A successful commit removes the raw capability from durable state and retains only protected verification material. Provider/root identity, durable fenced-mode markers, and bounded startup validation make copied, missing, corrupt, or legacy-active publication state fail closed. A higher fencing token compacts resolved older receipts, so long-lived coordinators do not exhaust receipt capacity.
|
|
396
402
|
|
|
397
|
-
Coordinators that do not yet have their own durable record can call `getDatabaseResourceFenceState({ databaseName })` before creating it. File-backed SmartDB returns the durable scope, highest token,
|
|
403
|
+
Coordinators that do not yet have their own durable record can call `getDatabaseResourceFenceState({ databaseName })` before creating it. File-backed SmartDB returns the durable scope, highest token, current publication phase, and allocation lifecycle/identity when the name is allocation-managed, or `null` only when both fence state and its durable marker are absent. The inspection never returns mutation IDs, mutation payload digests, publication receipts, or publication capabilities. Treat any active phase, identity mismatch, corrupt state, unsupported storage, or unsafe token as a hard stop rather than creating coordinator state.
|
|
404
|
+
|
|
405
|
+
### Authoritative Database Allocation
|
|
406
|
+
|
|
407
|
+
`allocateDatabaseTenant()` is the create-only control-plane API for permanently allocation-managed database names. It is available only when `allocationFencingVersion === 1`, `allocationFencingSupported === true`, and `allocationFencingRequiresDrain === true`. The request requires literal `expectedAbsent: true`, an `ISmartDbResourceFence`, and the tenant database, username, and password. `roles` is optional and defaults to `['readWrite', 'dbAdmin']`. SmartDB proves database-root and auth-principal absence while holding the database resource lock, maintenance write gate, and a bounded cross-process reservation on the durable auth store, then returns an `ISmartDbAllocateDatabaseTenantResult`; its `.allocation` field is the durable `ISmartDbDatabaseAllocationIdentity`. Exact fence replay returns the same allocation ID, generation, principal ID, and receipt digests.
|
|
408
|
+
|
|
409
|
+
Pass the returned `allocation` identity to every later `ensureDatabaseTenant()`, `importDatabase()`, and `deleteDatabaseTenant()` request for that name. Managed import and delete requests also require the exact allocation `username`; the identity alone is insufficient. Each distinct managed mutation requires a strictly newer fence token; the current token is accepted only for exact receipt replay or continuation, and older receipts become explicitly stale after advancement. Held publication receipts carry the allocation into `commitDatabasePublication()`. Managed requests without it and unmanaged requests with it fail closed. Deprovision persists `deprovisioning`, durably deletes the exact marked root, atomically removes only the exact principal, then persists a permanent `deprovisioned` tombstone. A later allocation requires a newer fence token, increments generation, and receives a fresh allocation and principal identity. Wire data access is available only while active and only to the current allocation principal; database/user ownership-changing wire commands are rejected.
|
|
410
|
+
|
|
411
|
+
The provider marker `.__rustdb_allocation.json` is created and preserved by SmartDB, never accepted from an import payload, and never included in logical exports. The resource lock uses the distinct `smartdb-resource-allocation-managed-v1` marker. Pre-allocation binaries, including 2.18.1, interpret that lock marker as invalid and fail closed. Consequently, N-1 rollback of a storage root after allocation fencing has touched a name is intentionally unavailable; restore the allocation-aware binary rather than removing or editing provider metadata.
|
|
398
412
|
|
|
399
413
|
`exportDatabase()` drains live MongoDB wire commands for the selected database and holds an exclusive local and cross-process database lease while producing the snapshot. The export is therefore consistent across all collections. It enforces server ceilings while visiting documents instead of first materializing an unbounded database; callers can request lower `maxEncodedBytes`, `maxCollections`, `maxDocuments`, and `maxIndexes` limits. The result emits canonical MongoDB Extended JSON so every BSON type, including 64-bit integers, survives the JSON management channel exactly. `importDatabase()` accepts canonical or relaxed Extended JSON. Database migration clients should preserve the exported objects as JSON values and must not coerce Extended JSON numeric wrappers into JavaScript numbers.
|
|
400
414
|
|
|
401
415
|
`getDatabaseContentDigest()` computes a bounded, database-name-independent SHA-256 over collection names, exact BSON document bytes, and persisted index specifications. Collection, document, and index enumeration order is normalized; BSON field order and compound-index key order remain significant. `ISmartDbGetDatabaseContentDigestInput` accepts `databaseName` and optional `ISmartDbDatabaseContentDigestLimits` fields `maxScannedBsonBytes`, `maxCollections`, `maxDocuments`, and `maxIndexes`. Callers may lower the server ceilings of 96 MiB scanned BSON, 10,000 collections, 1,000,000 documents, and 100,000 indexes. The `ISmartDbDatabaseContentDigest` result reports format `smartdb.database.content-digest.v1`, algorithm `sha256`, the lowercase digest, and exact scan counters.
|
|
402
416
|
|
|
403
|
-
|
|
417
|
+
`SmartdbServer.start()` and the lifecycle-critical health, fence-state, digest, export, import, publication-commit, tenant-allocation, tenant-ensure, and tenant-delete methods accept optional `ISmartDbManagementOperationOptions` with `signal?: AbortSignal` and `timeoutMs?: number`. `timeoutMs`, when provided, must be a positive safe integer no greater than `2,147,483,647`. Startup applies both cancellation and one deadline across legacy storage migration, sidecar spawn, auth metadata migration, and database readiness. Cancellation and deadlines are fail-stop once the sidecar is owned: SmartDB attempts to terminate the Rust engine before rejecting. Once termination is confirmed, the operation cannot continue after the caller releases ownership. If termination itself fails, the rejection retains bridge ownership and the service owner must retry `stop()` until it succeeds. After cancellation or deadline termination, restart SmartDB before accepting more traffic.
|
|
404
418
|
|
|
405
419
|
```typescript
|
|
406
420
|
const controller = new AbortController();
|
|
@@ -426,7 +440,7 @@ await client.db('admin').command({ usersInfo: 'reader' });
|
|
|
426
440
|
|
|
427
441
|
| Method / Property | Type | Description |
|
|
428
442
|
|---|---|---|
|
|
429
|
-
| `start()` | `Promise<void>` | Start
|
|
443
|
+
| `start(options?)` | `Promise<void>` | Start under one optional cancellation/deadline budget; join partial sidecars before rejection or retain ownership for `stop()` retry if termination fails |
|
|
430
444
|
| `stop()` | `Promise<void>` | Wait for active startup work, then stop the server and confirm Rust bridge cleanup, including partial startup cleanup |
|
|
431
445
|
| `getConnectionUri()` | `string` | Get the active `mongodb://` URI; before start and after stop, port `0` remains unresolved |
|
|
432
446
|
| `running` | `boolean` | Whether the server is currently running |
|
|
@@ -440,6 +454,7 @@ await client.db('admin').command({ usersInfo: 'reader' });
|
|
|
440
454
|
| `getCollections(db?)` | `Promise<ICollectionInfo[]>` | List all collections with counts |
|
|
441
455
|
| `getDocuments(db, coll, limit?, skip?)` | `Promise<IDocumentsResult>` | Browse documents with pagination |
|
|
442
456
|
| `getHealth(options?)` | `Promise<ISmartDbHealth>` | Read readiness and explicit publication-hold capability fields with optional fail-stop cancellation/deadline ownership |
|
|
457
|
+
| `allocateDatabaseTenant(params, options?)` | `Promise<ISmartDbAllocateDatabaseTenantResult>` | Authoritatively allocate an absent file-backed tenant and return its durable allocation identity; supports fail-stop cancellation and deadlines |
|
|
443
458
|
| `ensureDatabaseTenant(params, options?)` | `Promise<ISmartDbEnsureDatabaseTenantResult>` | Idempotently ensure an exact fenced tenant; supports fail-stop cancellation and deadlines |
|
|
444
459
|
| `getDatabaseResourceFenceState(params, options?)` | `Promise<ISmartDbDatabaseResourceFenceState \| null>` | Safely inspect a file-backed database fence high-water mark and publication phase without exposing receipts or capability material; supports fail-stop cancellation and deadlines |
|
|
445
460
|
| `getDatabaseContentDigest(params, options?)` | `Promise<ISmartDbDatabaseContentDigest>` | Compute a bounded, database-name-independent digest over exact BSON documents and persisted index specifications; supports fail-stop cancellation and deadlines |
|
package/readme.plan.md
CHANGED
|
@@ -77,6 +77,8 @@ Provable property: "PSS is a function of live data, not of history."
|
|
|
77
77
|
- **Offline toolbox**: `rustdb verify | dump | compact | repair-doc | backup` against a data directory.
|
|
78
78
|
- **On-disk format manifest** with versioning and an N-1 rollback contract: any release can downgrade one release; format changes require a migration note.
|
|
79
79
|
|
|
80
|
+
Allocation fencing exception: once a database name has an allocation-managed resource-lock marker, pre-allocation binaries intentionally fail closed. N-1 rollback must restore an allocation-aware binary; deleting or rewriting provider allocation metadata is not a supported downgrade path.
|
|
81
|
+
|
|
80
82
|
Provable property: "any resource question an operator asks during an incident is answerable from serverStatus or the toolbox."
|
|
81
83
|
|
|
82
84
|
## Fork in the road (decision rule, written now)
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/index.ts
CHANGED
|
@@ -40,11 +40,15 @@ export type {
|
|
|
40
40
|
ISmartDbCommitDatabasePublicationInput,
|
|
41
41
|
ISmartDbManagementOperationOptions,
|
|
42
42
|
ISmartDbDatabaseResourceFenceState,
|
|
43
|
+
ISmartDbDatabaseAllocationIdentity,
|
|
44
|
+
ISmartDbDatabaseAllocationLifecycle,
|
|
43
45
|
TSmartDbCommitDatabasePublicationResult,
|
|
44
46
|
TSmartDbResourceFenceErrorCode,
|
|
45
47
|
ISmartDbDatabaseTenantInput,
|
|
46
48
|
ISmartDbEnsureDatabaseTenantInput,
|
|
47
49
|
ISmartDbEnsureDatabaseTenantResult,
|
|
50
|
+
ISmartDbAllocateDatabaseTenantInput,
|
|
51
|
+
ISmartDbAllocateDatabaseTenantResult,
|
|
48
52
|
ISmartDbDeleteDatabaseTenantInput,
|
|
49
53
|
ISmartDbRotateDatabaseTenantPasswordInput,
|
|
50
54
|
ISmartDbDatabaseTenantDescriptor,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type IAuthMetadataPermissionMigrationBridge,
|
|
4
4
|
type TAuthMetadataPermissionMigrationStatus,
|
|
5
5
|
} from './migrators/v1_auth_metadata_permissions.js';
|
|
6
|
+
import type { ISmartDbManagementOperationOptions } from '../ts_smartdb/service-types.js';
|
|
6
7
|
|
|
7
8
|
const authMetadataMigrations = [migrateV1AuthMetadataPermissions] as const;
|
|
8
9
|
|
|
@@ -15,11 +16,12 @@ export class AuthMetadataMigrationRunner {
|
|
|
15
16
|
|
|
16
17
|
public async run(
|
|
17
18
|
bridgeArg: IAuthMetadataPermissionMigrationBridge,
|
|
19
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
18
20
|
): Promise<TAuthMetadataMigrationRunnerStatus> {
|
|
19
21
|
if (process.platform !== 'linux') return 'not-applicable';
|
|
20
22
|
let status: TAuthMetadataPermissionMigrationStatus = 'not-present';
|
|
21
23
|
for (const migration of authMetadataMigrations) {
|
|
22
|
-
status = await migration(bridgeArg, this.usersPath);
|
|
24
|
+
status = await migration(bridgeArg, this.usersPath, optionsArg);
|
|
23
25
|
}
|
|
24
26
|
return status;
|
|
25
27
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
3
|
import { migrateV0ToV1 } from './migrators/v0_to_v1.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -28,12 +28,14 @@ export class StorageMigrator {
|
|
|
28
28
|
/**
|
|
29
29
|
* Run any needed migrations. Safe to call even if storage is already current.
|
|
30
30
|
*/
|
|
31
|
-
async run(): Promise<void> {
|
|
32
|
-
|
|
31
|
+
async run(signalArg?: AbortSignal): Promise<void> {
|
|
32
|
+
signalArg?.throwIfAborted();
|
|
33
|
+
if (!await this.pathExists(this.storagePath)) {
|
|
33
34
|
return; // No data yet — nothing to migrate
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const version = this.detectVersion();
|
|
37
|
+
const version = await this.detectVersion(signalArg);
|
|
38
|
+
signalArg?.throwIfAborted();
|
|
37
39
|
|
|
38
40
|
if (version === 1) {
|
|
39
41
|
return; // Already current
|
|
@@ -43,7 +45,8 @@ export class StorageMigrator {
|
|
|
43
45
|
console.log(`[smartdb] Detected v0 (JSON) storage format at ${this.storagePath}`);
|
|
44
46
|
console.log(`[smartdb] Running migration v0 → v1 (Bitcask binary format)...`);
|
|
45
47
|
|
|
46
|
-
const deletableFiles = await migrateV0ToV1(this.storagePath);
|
|
48
|
+
const deletableFiles = await migrateV0ToV1(this.storagePath, signalArg);
|
|
49
|
+
signalArg?.throwIfAborted();
|
|
47
50
|
|
|
48
51
|
if (deletableFiles.length > 0) {
|
|
49
52
|
console.log(`[smartdb] Migration v0 → v1 complete.`);
|
|
@@ -63,33 +66,49 @@ export class StorageMigrator {
|
|
|
63
66
|
* v0: {db}/{coll}.json files exist
|
|
64
67
|
* v1: {db}/{coll}/data.rdb directories exist
|
|
65
68
|
*/
|
|
66
|
-
private detectVersion(): TStorageVersion {
|
|
67
|
-
|
|
69
|
+
private async detectVersion(signalArg?: AbortSignal): Promise<TStorageVersion> {
|
|
70
|
+
signalArg?.throwIfAborted();
|
|
71
|
+
const entries = await fs.readdir(this.storagePath, { withFileTypes: true });
|
|
72
|
+
let hasLegacyCollections = false;
|
|
68
73
|
|
|
69
74
|
for (const entry of entries) {
|
|
75
|
+
signalArg?.throwIfAborted();
|
|
70
76
|
if (!entry.isDirectory()) continue;
|
|
71
77
|
if (entry.name.startsWith('.__rustdb_')) continue;
|
|
72
78
|
|
|
73
79
|
const dbDir = path.join(this.storagePath, entry.name);
|
|
74
|
-
const dbEntries = fs.
|
|
80
|
+
const dbEntries = await fs.readdir(dbDir, { withFileTypes: true });
|
|
75
81
|
|
|
76
82
|
for (const dbEntry of dbEntries) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
signalArg?.throwIfAborted();
|
|
84
|
+
if (
|
|
85
|
+
dbEntry.isDirectory()
|
|
86
|
+
&& dbEntry.name.startsWith('.__rustdb_migration_')
|
|
87
|
+
) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`SmartDB legacy migration staging is orphaned: ${path.join(dbDir, dbEntry.name)}`,
|
|
90
|
+
);
|
|
84
91
|
}
|
|
92
|
+
if (dbEntry.name.startsWith('.__rustdb_')) continue;
|
|
85
93
|
// v0: .json file (not .indexes.json)
|
|
86
94
|
if (dbEntry.isFile() && dbEntry.name.endsWith('.json') && !dbEntry.name.endsWith('.indexes.json')) {
|
|
87
|
-
|
|
95
|
+
hasLegacyCollections = true;
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
//
|
|
93
|
-
|
|
100
|
+
// A mixed layout is an interrupted/resumable migration, not proof that the
|
|
101
|
+
// complete storage root is already current.
|
|
102
|
+
return hasLegacyCollections ? 0 : 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async pathExists(pathArg: string): Promise<boolean> {
|
|
106
|
+
try {
|
|
107
|
+
await fs.stat(pathArg);
|
|
108
|
+
return true;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
94
113
|
}
|
|
95
114
|
}
|
package/ts/ts_migration/index.ts
CHANGED