@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.
Files changed (40) hide show
  1. package/dist_rust/rustdb_linux_amd64 +0 -0
  2. package/dist_rust/rustdb_linux_arm64 +0 -0
  3. package/dist_ts/00_commitinfo_data.js +2 -2
  4. package/dist_ts/index.d.ts +1 -1
  5. package/dist_ts/index.js +1 -1
  6. package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.d.ts +2 -1
  7. package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.js +3 -3
  8. package/dist_ts/ts_migration/classes.storagemigrator.d.ts +2 -1
  9. package/dist_ts/ts_migration/classes.storagemigrator.js +36 -20
  10. package/dist_ts/ts_migration/index.d.ts +1 -0
  11. package/dist_ts/ts_migration/index.js +2 -1
  12. package/dist_ts/ts_migration/migrators/v0_to_v1.d.ts +7 -1
  13. package/dist_ts/ts_migration/migrators/v0_to_v1.js +162 -47
  14. package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.d.ts +3 -2
  15. package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.js +3 -3
  16. package/dist_ts/ts_smartdb/index.d.ts +1 -1
  17. package/dist_ts/ts_smartdb/index.js +1 -1
  18. package/dist_ts/ts_smartdb/resource-fencing.d.ts +12 -1
  19. package/dist_ts/ts_smartdb/resource-fencing.js +44 -2
  20. package/dist_ts/ts_smartdb/rust-db-bridge.d.ts +5 -4
  21. package/dist_ts/ts_smartdb/rust-db-bridge.js +33 -10
  22. package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +4 -2
  23. package/dist_ts/ts_smartdb/server/SmartdbServer.js +84 -9
  24. package/dist_ts/ts_smartdb/service-types.d.ts +47 -2
  25. package/dist_ts/ts_smartdb/service-types.js +7 -2
  26. package/package.json +1 -1
  27. package/readme.md +19 -4
  28. package/readme.plan.md +2 -0
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/index.ts +4 -0
  31. package/ts/ts_migration/classes.authmetadatamigrationrunner.ts +3 -1
  32. package/ts/ts_migration/classes.storagemigrator.ts +38 -19
  33. package/ts/ts_migration/index.ts +1 -0
  34. package/ts/ts_migration/migrators/v0_to_v1.ts +224 -44
  35. package/ts/ts_migration/migrators/v1_auth_metadata_permissions.ts +4 -0
  36. package/ts/ts_smartdb/index.ts +4 -0
  37. package/ts/ts_smartdb/resource-fencing.ts +59 -2
  38. package/ts/ts_smartdb/rust-db-bridge.ts +56 -11
  39. package/ts/ts_smartdb/server/SmartdbServer.ts +105 -9
  40. package/ts/ts_smartdb/service-types.ts +59 -2
@@ -2,7 +2,12 @@ import {
2
2
  RustDbBridge,
3
3
  SmartDbManagementOperationTerminationError,
4
4
  } from '../rust-db-bridge.js';
5
- import { AuthMetadataMigrationRunner, StorageMigrator } from '../../ts_migration/index.js';
5
+ import {
6
+ AuthMetadataMigrationRunner,
7
+ SmartDbStorageMigrationCleanupError,
8
+ StorageMigrator,
9
+ } from '../../ts_migration/index.js';
10
+ import { validateSmartDbManagementOperationOptions } from '../offline-inspection.js';
6
11
  import type {
7
12
  IOpLogEntry,
8
13
  IOpLogResult,
@@ -17,6 +22,8 @@ import type {
17
22
  ISmartDbDatabaseTenantInput,
18
23
  ISmartDbEnsureDatabaseTenantInput,
19
24
  ISmartDbEnsureDatabaseTenantResult,
25
+ ISmartDbAllocateDatabaseTenantInput,
26
+ ISmartDbAllocateDatabaseTenantResult,
20
27
  ISmartDbDeleteDatabaseTenantInput,
21
28
  ISmartDbRotateDatabaseTenantPasswordInput,
22
29
  ISmartDbDatabaseTenantDescriptor,
@@ -146,11 +153,63 @@ export class SmartdbServer {
146
153
  /**
147
154
  * Start the server
148
155
  */
149
- async start(): Promise<void> {
156
+ async start(optionsArg?: ISmartDbManagementOperationOptions): Promise<void> {
157
+ validateSmartDbManagementOperationOptions(optionsArg);
158
+ optionsArg?.signal?.throwIfAborted();
150
159
  if (this.isRunning || this.startInProgress || this.bridgeOwned) {
151
160
  throw new Error('Server is already running or still owns a Rust bridge');
152
161
  }
153
162
 
163
+ const startupStartedAt = Date.now();
164
+ const startupDeadline = optionsArg?.timeoutMs === undefined
165
+ ? undefined
166
+ : startupStartedAt + optionsArg.timeoutMs;
167
+ const timeoutError = new DOMException(
168
+ optionsArg?.timeoutMs === undefined
169
+ ? 'SmartDB startup timed out'
170
+ : `SmartDB startup timed out after ${optionsArg.timeoutMs}ms`,
171
+ 'TimeoutError',
172
+ );
173
+ const startupAbortController = new AbortController();
174
+ let startupTimeout: ReturnType<typeof setTimeout> | undefined;
175
+ let externalAbortHandler: (() => void) | undefined;
176
+ if (optionsArg?.signal) {
177
+ externalAbortHandler = () => {
178
+ startupAbortController.abort(optionsArg.signal!.reason);
179
+ };
180
+ optionsArg.signal.addEventListener('abort', externalAbortHandler, { once: true });
181
+ }
182
+ if (optionsArg?.timeoutMs !== undefined) {
183
+ startupTimeout = setTimeout(() => {
184
+ startupAbortController.abort(timeoutError);
185
+ }, optionsArg.timeoutMs);
186
+ }
187
+ const checkpoint = () => {
188
+ if (
189
+ startupDeadline !== undefined
190
+ && Date.now() >= startupDeadline
191
+ && !startupAbortController.signal.aborted
192
+ ) {
193
+ startupAbortController.abort(timeoutError);
194
+ }
195
+ startupAbortController.signal.throwIfAborted();
196
+ };
197
+ const getBridgeOperationOptions = (): ISmartDbManagementOperationOptions | undefined => {
198
+ checkpoint();
199
+ const remainingTimeoutMs = startupDeadline === undefined
200
+ ? undefined
201
+ : startupDeadline - Date.now();
202
+ if (remainingTimeoutMs !== undefined && remainingTimeoutMs <= 0) {
203
+ startupAbortController.abort(timeoutError);
204
+ checkpoint();
205
+ }
206
+ if (!optionsArg?.signal && remainingTimeoutMs === undefined) return undefined;
207
+ return {
208
+ ...(optionsArg?.signal ? { signal: optionsArg.signal } : {}),
209
+ ...(remainingTimeoutMs === undefined ? {} : { timeoutMs: remainingTimeoutMs }),
210
+ };
211
+ };
212
+
154
213
  this.startInProgress = true;
155
214
  let resolveStartCompletion!: () => void;
156
215
  const startCompletionPromise = new Promise<void>((resolveArg) => {
@@ -158,16 +217,19 @@ export class SmartdbServer {
158
217
  });
159
218
  this.startCompletionPromise = startCompletionPromise;
160
219
  try {
220
+ checkpoint();
161
221
  // Run storage migration for file-based storage before starting Rust engine
162
222
  if (this.options.storage === 'file' && this.options.storagePath) {
163
223
  const migrator = new StorageMigrator(this.options.storagePath);
164
- await migrator.run();
224
+ await migrator.run(startupAbortController.signal);
225
+ checkpoint();
165
226
  }
166
227
 
167
228
  // Ownership starts before spawn because a rejected spawn can still have
168
229
  // created a child whose termination must be confirmed.
169
230
  this.bridgeOwned = true;
170
- const spawned = await this.bridge.spawn();
231
+ const spawned = await this.bridge.spawn(getBridgeOperationOptions());
232
+ checkpoint();
171
233
  if (!spawned) {
172
234
  throw new Error(
173
235
  'smartdb Rust binary not found. Set SMARTDB_RUST_BINARY env var, ' +
@@ -179,7 +241,11 @@ export class SmartdbServer {
179
241
  const authMetadataMigrationRunner = new AuthMetadataMigrationRunner(
180
242
  this.options.auth.usersPath,
181
243
  );
182
- await authMetadataMigrationRunner.run(this.bridge);
244
+ await authMetadataMigrationRunner.run(
245
+ this.bridge,
246
+ getBridgeOperationOptions(),
247
+ );
248
+ checkpoint();
183
249
  }
184
250
 
185
251
  // Send config, get back connectionUri
@@ -194,7 +260,8 @@ export class SmartdbServer {
194
260
  auth: this.options.auth,
195
261
  tls: this.options.tls,
196
262
  oplog: this.options.oplog,
197
- });
263
+ }, getBridgeOperationOptions());
264
+ checkpoint();
198
265
  if (!this.bridgeOwned || !this.bridge.running) {
199
266
  throw new Error('smartdb Rust process exited during startup');
200
267
  }
@@ -214,8 +281,19 @@ export class SmartdbServer {
214
281
  this.resolvedPort = resolvedPort;
215
282
  }
216
283
  this.resolvedConnectionUri = result.connectionUri;
284
+ checkpoint();
217
285
  this.isRunning = true;
218
286
  } catch (error) {
287
+ const startupError = error instanceof SmartDbManagementOperationTerminationError
288
+ || error instanceof SmartDbStorageMigrationCleanupError
289
+ ? error
290
+ : optionsArg?.signal?.aborted
291
+ ? optionsArg.signal.reason
292
+ : startupAbortController.signal.aborted || (
293
+ startupDeadline !== undefined && Date.now() >= startupDeadline
294
+ )
295
+ ? timeoutError
296
+ : error;
219
297
  this.isRunning = false;
220
298
  this.resolvedConnectionUri = '';
221
299
  this.resolvedPort = undefined;
@@ -225,14 +303,18 @@ export class SmartdbServer {
225
303
  this.bridgeOwned = false;
226
304
  } catch (cleanupError) {
227
305
  throw new AggregateError(
228
- [error, cleanupError],
306
+ [startupError, cleanupError],
229
307
  'SmartDB startup failed and Rust bridge cleanup was incomplete',
230
- { cause: error },
308
+ { cause: startupError },
231
309
  );
232
310
  }
233
311
  }
234
- throw error;
312
+ throw startupError;
235
313
  } finally {
314
+ if (startupTimeout) clearTimeout(startupTimeout);
315
+ if (externalAbortHandler && optionsArg?.signal) {
316
+ optionsArg.signal.removeEventListener('abort', externalAbortHandler);
317
+ }
236
318
  this.startInProgress = false;
237
319
  resolveStartCompletion();
238
320
  if (this.startCompletionPromise === startCompletionPromise) {
@@ -345,6 +427,17 @@ export class SmartdbServer {
345
427
  return this.withTenantMongoUri(descriptor, params.password);
346
428
  }
347
429
 
430
+ /** Authoritatively allocate an absent, fenced database tenant. */
431
+ async allocateDatabaseTenant(
432
+ params: ISmartDbAllocateDatabaseTenantInput,
433
+ optionsArg?: ISmartDbManagementOperationOptions,
434
+ ): Promise<ISmartDbAllocateDatabaseTenantResult> {
435
+ const descriptor = await this.runDatabaseManagementOperation(
436
+ () => this.bridge.allocateDatabaseTenant(params, optionsArg),
437
+ );
438
+ return this.withTenantMongoUri(descriptor, params.password);
439
+ }
440
+
348
441
  /**
349
442
  * Idempotently provision or rotate one exclusively owned database tenant.
350
443
  */
@@ -465,6 +558,9 @@ export class SmartdbServer {
465
558
  resourceFencingVersion: this.options.storage === 'file' ? 1 : undefined,
466
559
  resourceFencingSupported: this.options.storage === 'file' ? false : undefined,
467
560
  resourceFencingRequiresDrain: this.options.storage === 'file' ? true : undefined,
561
+ allocationFencingVersion: this.options.storage === 'file' ? 1 : undefined,
562
+ allocationFencingSupported: this.options.storage === 'file' ? false : undefined,
563
+ allocationFencingRequiresDrain: this.options.storage === 'file' ? true : undefined,
468
564
  publicationHoldVersion: 1,
469
565
  publicationHoldSupported: false,
470
566
  publicationHoldRequiresExternalDrain: false,
@@ -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;
@@ -53,6 +56,11 @@ export const smartDbResourceFenceErrorCodes = [
53
56
  'EFENCE_PUBLICATION_HELD',
54
57
  'EFENCE_CAPABILITY_MISMATCH',
55
58
  'EFENCE_NOT_HELD',
59
+ 'EALLOCATION_EXPECTED_ABSENT_CONFLICT',
60
+ 'EALLOCATION_REQUIRED',
61
+ 'EALLOCATION_MISMATCH',
62
+ 'EALLOCATION_GENERATION_EXHAUSTED',
63
+ 'EALLOCATION_DEPROVISIONED',
56
64
  ] as const;
57
65
 
58
66
  export type TSmartDbResourceFenceErrorCode =
@@ -71,7 +79,8 @@ export interface ISmartDbResourceFenceReceipt
71
79
  kind:
72
80
  | 'smartdb.database.replace.v1'
73
81
  | 'smartdb.database.delete.v1'
74
- | 'smartdb.database.ensure.v1';
82
+ | 'smartdb.database.ensure.v1'
83
+ | 'smartdb.database.allocate.v1';
75
84
  /** Digest of the canonical payload interpretation derived by SmartDB. */
76
85
  effectivePayloadSha256: string;
77
86
  provider?: 'smartdb';
@@ -84,6 +93,35 @@ export interface ISmartDbResourceFenceReceipt
84
93
  publicationCapabilitySha256?: string;
85
94
  /** Present only while publication is held; never returned after release. */
86
95
  publicationCapability?: string;
96
+ allocation?: ISmartDbDatabaseAllocationIdentity;
97
+ }
98
+
99
+ export interface ISmartDbDatabaseAllocationIdentity {
100
+ version: 1;
101
+ provider: 'smartdb';
102
+ state: 'active';
103
+ databaseName: string;
104
+ username: string;
105
+ allocationId: string;
106
+ generation: number;
107
+ principalId: string;
108
+ resourceIdSha256: string;
109
+ bindingSha256: string;
110
+ receiptSha256: string;
111
+ }
112
+
113
+ export interface ISmartDbDatabaseAllocationLifecycle {
114
+ version: 1;
115
+ provider: 'smartdb';
116
+ state: 'allocating' | 'active' | 'deprovisioning' | 'deprovisioned';
117
+ databaseName: string;
118
+ username: string;
119
+ allocationId: string;
120
+ generation: number;
121
+ principalId: string;
122
+ resourceIdSha256: string;
123
+ bindingSha256: string;
124
+ receiptSha256: string;
87
125
  }
88
126
 
89
127
  export interface ISmartDbHeldPublicationReceipt
@@ -121,6 +159,7 @@ export interface ISmartDbDatabaseResourceFenceState {
121
159
  scopeId: string;
122
160
  highestToken: number;
123
161
  publicationState: 'requested' | 'publishing' | 'held' | 'released';
162
+ allocation?: ISmartDbDatabaseAllocationLifecycle;
124
163
  }
125
164
 
126
165
  export class SmartDbResourceFenceError extends Error {
@@ -144,7 +183,7 @@ export const normalizeSmartDbResourceFenceError = (
144
183
  errorArg instanceof Error
145
184
  ? errorArg
146
185
  : new Error(String(errorArg));
147
- const match = /^(EFENCE_[A-Z_]+):\s*(.*)$/s.exec(error.message);
186
+ const match = /^((?:EFENCE|EALLOCATION)_[A-Z_]+):\s*(.*)$/s.exec(error.message);
148
187
  if (
149
188
  !match ||
150
189
  !smartDbResourceFenceErrorCodes.includes(
@@ -170,6 +209,13 @@ export interface ISmartDbDatabaseTenantInput {
170
209
  export interface ISmartDbEnsureDatabaseTenantInput
171
210
  extends ISmartDbDatabaseTenantInput {
172
211
  fence: ISmartDbResourceFence;
212
+ allocation?: ISmartDbDatabaseAllocationIdentity;
213
+ }
214
+
215
+ export interface ISmartDbAllocateDatabaseTenantInput
216
+ extends ISmartDbDatabaseTenantInput {
217
+ expectedAbsent: true;
218
+ fence: ISmartDbResourceFence;
173
219
  }
174
220
 
175
221
  export interface ISmartDbDeleteDatabaseTenantInput {
@@ -184,6 +230,7 @@ export interface ISmartDbDeleteDatabaseTenantInput {
184
230
  */
185
231
  username?: string;
186
232
  fence?: ISmartDbResourceFence;
233
+ allocation?: ISmartDbDatabaseAllocationIdentity;
187
234
  /** Keep the database data plane closed until commitDatabasePublication. */
188
235
  holdPublication?: boolean;
189
236
  }
@@ -200,6 +247,7 @@ export interface ISmartDbDatabaseTenantDescriptor {
200
247
  authSource: string;
201
248
  mongodbUri?: string;
202
249
  resourceFence?: ISmartDbResourceFenceReceipt;
250
+ allocation?: ISmartDbDatabaseAllocationIdentity;
203
251
  }
204
252
 
205
253
  export interface ISmartDbEnsureDatabaseTenantResult
@@ -207,6 +255,12 @@ export interface ISmartDbEnsureDatabaseTenantResult
207
255
  resourceFence: ISmartDbResourceFenceReceipt;
208
256
  }
209
257
 
258
+ export interface ISmartDbAllocateDatabaseTenantResult
259
+ extends ISmartDbDatabaseTenantDescriptor {
260
+ resourceFence: ISmartDbResourceFenceReceipt;
261
+ allocation: ISmartDbDatabaseAllocationIdentity;
262
+ }
263
+
210
264
  export interface ISmartDbDeleteDatabaseTenantResult {
211
265
  databaseName: string;
212
266
  /**
@@ -222,6 +276,7 @@ export interface ISmartDbDeleteDatabaseTenantResult {
222
276
  */
223
277
  usersPreserved?: boolean;
224
278
  resourceFence?: ISmartDbResourceFenceReceipt;
279
+ allocation?: ISmartDbDatabaseAllocationIdentity;
225
280
  }
226
281
 
227
282
  export interface ISmartDbDatabaseExportCollection {
@@ -282,6 +337,7 @@ export interface ISmartDbImportDatabaseInput {
282
337
  */
283
338
  username?: string;
284
339
  fence?: ISmartDbResourceFence;
340
+ allocation?: ISmartDbDatabaseAllocationIdentity;
285
341
  /** Keep the database data plane closed until commitDatabasePublication. */
286
342
  holdPublication?: boolean;
287
343
  }
@@ -291,6 +347,7 @@ export interface ISmartDbImportDatabaseResult {
291
347
  collections: number;
292
348
  documents: number;
293
349
  resourceFence?: ISmartDbResourceFenceReceipt;
350
+ allocation?: ISmartDbDatabaseAllocationIdentity;
294
351
  }
295
352
 
296
353
  export type TSmartDbCommitDatabasePublicationResult =