@lossless.org/nosqldb 10.10.0 → 11.0.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.
Files changed (43) hide show
  1. package/.smartconfig.json +21 -19
  2. package/dist_rust/rustdb_linux_amd64 +0 -0
  3. package/dist_rust/rustdb_linux_amd64.tsrust-build.json +5 -5
  4. package/dist_rust/rustdb_linux_amd64_musl +0 -0
  5. package/dist_rust/rustdb_linux_amd64_musl.tsrust-build.json +5 -5
  6. package/dist_rust/rustdb_linux_arm64 +0 -0
  7. package/dist_rust/rustdb_linux_arm64.tsrust-build.json +5 -5
  8. package/dist_rust/rustdb_linux_arm64_musl +0 -0
  9. package/dist_rust/rustdb_linux_arm64_musl.tsrust-build.json +5 -5
  10. package/dist_rust/rustdb_macos_amd64 +0 -0
  11. package/dist_rust/rustdb_macos_amd64.tsrust-build.json +5 -5
  12. package/dist_rust/rustdb_macos_arm64 +0 -0
  13. package/dist_rust/rustdb_macos_arm64.tsrust-build.json +5 -5
  14. package/dist_ts/00_commitinfo_data.js +2 -2
  15. package/dist_ts/ts_local/classes.localnosqldb.d.ts +15 -4
  16. package/dist_ts/ts_local/classes.localnosqldb.js +31 -11
  17. package/dist_ts/ts_nosqldb/index.d.ts +3 -3
  18. package/dist_ts/ts_nosqldb/index.js +5 -4
  19. package/dist_ts/ts_nosqldb/rust-db-bridge.d.ts +125 -2
  20. package/dist_ts/ts_nosqldb/rust-db-bridge.js +261 -46
  21. package/dist_ts/ts_nosqldb/server/classes.nosqldbserver.d.ts +69 -3
  22. package/dist_ts/ts_nosqldb/server/classes.nosqldbserver.js +91 -12
  23. package/dist_ts/ts_nosqldb/server/index.d.ts +2 -2
  24. package/dist_ts/ts_nosqldb/server/index.js +2 -2
  25. package/dist_ts/ts_nosqldb/stopped-storage.d.ts +9 -1
  26. package/dist_ts/ts_nosqldb/stopped-storage.js +1 -1
  27. package/dist_ts/ts_nosqldb/storage-rehearsal.js +1 -1
  28. package/dist_ts/ts_nosqldb/storage-root-alignment.js +7 -3
  29. package/dist_ts/ts_nosqldb/storage-root-relocation.js +2 -2
  30. package/package.json +20 -16
  31. package/readme.md +469 -666
  32. package/third-party-notices.md +40 -8
  33. package/third_party_licenses/inventory.json +6 -6
  34. package/ts/00_commitinfo_data.ts +1 -1
  35. package/ts/ts_local/classes.localnosqldb.ts +35 -12
  36. package/ts/ts_nosqldb/index.ts +7 -2
  37. package/ts/ts_nosqldb/rust-db-bridge.ts +291 -48
  38. package/ts/ts_nosqldb/server/classes.nosqldbserver.ts +151 -15
  39. package/ts/ts_nosqldb/server/index.ts +2 -2
  40. package/ts/ts_nosqldb/stopped-storage.ts +9 -1
  41. package/ts/ts_nosqldb/storage-rehearsal.ts +1 -1
  42. package/ts/ts_nosqldb/storage-root-alignment.ts +6 -2
  43. package/ts/ts_nosqldb/storage-root-relocation.ts +1 -1
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  RustDbBridge,
3
+ NoSqlDbEngineExitError,
3
4
  NoSqlDbManagementOperationTerminationError,
5
+ attachNoSqlDbEngineStderrTail,
4
6
  } from '../rust-db-bridge.js';
5
7
  import { validateNoSqlDbManagementOperationOptions } from '../offline-inspection.js';
6
8
  import type { INoSqlDbGetCommittedWritesInput, INoSqlDbCommittedWritesPage } from '../committed-writes.js';
@@ -83,6 +85,32 @@ export interface INoSqlDbServerOptions {
83
85
  query?: INoSqlDbQueryOptions;
84
86
  /** Memory budgets for received requests (default: sized from the memory limit). */
85
87
  inbound?: INoSqlDbInboundOptions;
88
+ /**
89
+ * The store's memory budget in bytes, which its node cache and write buffer
90
+ * share: 4 MiB to 1 TiB. The engine refuses a start with a value outside
91
+ * that range and never clamps it. Unset, the budget is computed from the
92
+ * memory the process may use.
93
+ */
94
+ storageBudgetBytes?: number;
95
+ /**
96
+ * Receives every line the engine writes to its standard error, in order and
97
+ * without its line break. Without it, nothing is written to the host
98
+ * process's standard error. Start failures and unexpected engine exits
99
+ * carry the engine's last lines either way, in `engineStderrTail`. Each
100
+ * call runs in its own microtask; an exception it throws is not caught and
101
+ * reaches the process as an uncaught exception.
102
+ */
103
+ engineLog?: (line: string) => void;
104
+ /**
105
+ * Called once for each unexpected engine exit: an exit with a non-zero
106
+ * status or by a signal while the server runs and no stop is in progress.
107
+ * An exit with status 0, which the engine makes after a graceful shutdown
108
+ * it was signalled to make, is not reported; the server counts as stopped
109
+ * either way. The call runs in a microtask after the server has recorded the
110
+ * exit. An exception it throws is not caught and reaches the process as an
111
+ * uncaught exception.
112
+ */
113
+ onEngineExit?: (error: NoSqlDbEngineExitError) => void;
86
114
  }
87
115
 
88
116
  export interface INoSqlDbOplogOptions {
@@ -115,6 +143,59 @@ export interface INoSqlDbTlsOptions {
115
143
  requireClientCert?: boolean;
116
144
  }
117
145
 
146
+ /**
147
+ * How `stop()` ended the engine. A stop is clean exactly when a stop request
148
+ * was sent and acknowledged, the server neither signalled nor killed the
149
+ * engine, and the engine exited with code 0 and no signal; `clean` says so.
150
+ */
151
+ export interface INoSqlDbServerStopResult {
152
+ /** Whether a stop request was sent to a running engine. */
153
+ stopRequestSent: boolean;
154
+ /** Whether the engine acknowledged that request. */
155
+ stopAcknowledged: boolean;
156
+ /**
157
+ * Whether the server had to signal or kill the engine to end it: the engine
158
+ * had not exited by itself within `noSqlDbServerStopGraceMs` of an
159
+ * acknowledged request, or no request was acknowledged while it ran.
160
+ */
161
+ forcedByServer: boolean;
162
+ /** The engine's exit code as its process exit reported it, or null. */
163
+ exitCode: number | null;
164
+ /** The signal that ended the engine as its process exit reported it, or null. */
165
+ exitSignal: string | null;
166
+ /** Whether the stop was clean, as defined above. */
167
+ clean: boolean;
168
+ }
169
+
170
+ /**
171
+ * How long `stop()` waits, after the engine has acknowledged the stop request
172
+ * and its management input is closed, for the engine to exit by itself before
173
+ * the server ends it.
174
+ */
175
+ export const noSqlDbServerStopGraceMs = 10_000;
176
+
177
+ /** One stop's result, frozen, as all of its callers share it. */
178
+ const stopResult = (
179
+ stopRequestSent: boolean,
180
+ stopAcknowledged: boolean,
181
+ forcedByServer: boolean,
182
+ exit: { code: number | null; signal: string | null } | null,
183
+ ): INoSqlDbServerStopResult => {
184
+ const exitCode = exit?.code ?? null;
185
+ const exitSignal = exit?.signal ?? null;
186
+ return Object.freeze({
187
+ stopRequestSent,
188
+ stopAcknowledged,
189
+ forcedByServer,
190
+ exitCode,
191
+ exitSignal,
192
+ clean: stopRequestSent && stopAcknowledged && !forcedByServer && exitCode === 0 && exitSignal === null,
193
+ });
194
+ };
195
+
196
+ /** The result of a stop when no engine runs: no request was sent. */
197
+ export const noStopRequestResult = stopResult(false, false, false, null);
198
+
118
199
  /**
119
200
  * NoSqlDbServer - Wire protocol compatible database server backed by Rust
120
201
  *
@@ -140,7 +221,7 @@ export class NoSqlDbServer {
140
221
  private bridgeOwned = false;
141
222
  private startInProgress = false;
142
223
  private startCompletionPromise: Promise<void> | null = null;
143
- private stopCompletionPromise: Promise<void> | null = null;
224
+ private stopCompletionPromise: Promise<INoSqlDbServerStopResult> | null = null;
144
225
  private isRunning = false;
145
226
  private resolvedConnectionUri = '';
146
227
  private resolvedPort: number | undefined;
@@ -162,25 +243,50 @@ export class NoSqlDbServer {
162
243
  oplog: options.oplog,
163
244
  query: options.query,
164
245
  inbound: options.inbound,
246
+ storageBudgetBytes: options.storageBudgetBytes,
247
+ engineLog: options.engineLog,
248
+ onEngineExit: options.onEngineExit,
165
249
  };
166
- this.bridge = new RustDbBridge({ binaryPath: this.options.binaryPath });
250
+ this.bridge = new RustDbBridge({ binaryPath: this.options.binaryPath, engineLog: this.options.engineLog });
167
251
  this.bridge.on('exit', (code: number | null, signal: string | null) => {
252
+ // A stop keeps the running state until it has confirmed the bridge's
253
+ // termination, also when the engine exits by itself first.
254
+ if (this.stopCompletionPromise) return;
168
255
  const exitedUnexpectedly = this.isRunning
169
- && !this.stopCompletionPromise
170
256
  && (code !== 0 || signal !== null);
171
257
  this.isRunning = false;
172
258
  this.resolvedConnectionUri = '';
173
259
  this.resolvedPort = undefined;
174
260
  if (exitedUnexpectedly) {
175
- console.error(`nosqldb Rust process exited unexpectedly (code=${code}, signal=${signal})`);
261
+ const onEngineExit = this.options.onEngineExit;
262
+ const exit = new NoSqlDbEngineExitError(code, signal, this.bridge.engineStderrTail);
263
+ // Outside the bridge's exit event, so a throwing callback cannot
264
+ // interrupt the bridge's own exit handling.
265
+ if (onEngineExit) queueMicrotask(() => onEngineExit(exit));
176
266
  }
177
267
  });
178
268
  }
179
269
 
180
270
  /**
181
- * Start the server
271
+ * Start the server. A rejection carries `engineStderrTail`: the last
272
+ * standard-error lines of the engine this start spawned, or none.
182
273
  */
183
274
  async start(optionsArg?: INoSqlDbManagementOperationOptions): Promise<void> {
275
+ const attempt = { engineSpawned: false };
276
+ try {
277
+ await this.startAttempt(optionsArg, attempt);
278
+ } catch (error) {
279
+ // Read after the attempt's cleanup termination, once the engine's last
280
+ // lines have arrived.
281
+ attachNoSqlDbEngineStderrTail(error, attempt.engineSpawned ? this.bridge.engineStderrTail : []);
282
+ throw error;
283
+ }
284
+ }
285
+
286
+ private async startAttempt(
287
+ optionsArg: INoSqlDbManagementOperationOptions | undefined,
288
+ attempt: { engineSpawned: boolean },
289
+ ): Promise<void> {
184
290
  validateNoSqlDbManagementOperationOptions(optionsArg);
185
291
  optionsArg?.signal?.throwIfAborted();
186
292
  if (this.isRunning || this.startInProgress || this.bridgeOwned || this.stopCompletionPromise) {
@@ -255,13 +361,13 @@ export class NoSqlDbServer {
255
361
  // Ownership starts before spawn because a rejected spawn can still have
256
362
  // created a child whose termination must be confirmed.
257
363
  this.bridgeOwned = true;
258
- const spawned = await this.bridge.spawn(getBridgeOperationOptions());
364
+ const spawnOptions = getBridgeOperationOptions();
365
+ attempt.engineSpawned = true;
366
+ const spawned = await this.bridge.spawn(spawnOptions);
259
367
  checkpoint();
260
368
  if (!spawned) {
261
- throw new Error(
262
- 'nosqldb Rust binary not found. Set NOSQLDB_RUST_BINARY env var, ' +
263
- 'install the platform package, or build locally with `tsrust`.'
264
- );
369
+ // The bridge states what it knows: that the engine at its binaryPath did not report ready, or, without one, both meanings.
370
+ throw new Error(this.bridge.spawnFailureMessage ?? 'the nosqldb engine did not start: spawn() returned false');
265
371
  }
266
372
 
267
373
  // Send config, get back connectionUri
@@ -278,6 +384,7 @@ export class NoSqlDbServer {
278
384
  oplog: this.options.oplog,
279
385
  query: this.options.query,
280
386
  inbound: this.options.inbound,
387
+ storageBudgetBytes: this.options.storageBudgetBytes,
281
388
  }, getBridgeOperationOptions());
282
389
  checkpoint();
283
390
  if (!this.bridgeOwned || !this.bridge.running) {
@@ -341,9 +448,15 @@ export class NoSqlDbServer {
341
448
  }
342
449
 
343
450
  /**
344
- * Stop the server
451
+ * Stop the server: wait for active startup work, send the engine a stop
452
+ * request, close its management input once it has acknowledged, give it
453
+ * `noSqlDbServerStopGraceMs` to exit by itself, and end it otherwise; then
454
+ * confirm the bridge's cleanup. Resolves with how the engine ended, which
455
+ * concurrent callers share; when no engine runs, the result says that no
456
+ * request was sent. Rejects, retaining ownership for a retry, only when the
457
+ * bridge's termination fails.
345
458
  */
346
- stop(): Promise<void> {
459
+ stop(): Promise<INoSqlDbServerStopResult> {
347
460
  if (this.stopCompletionPromise) {
348
461
  return this.stopCompletionPromise;
349
462
  }
@@ -359,30 +472,53 @@ export class NoSqlDbServer {
359
472
  return stopCompletionPromise;
360
473
  }
361
474
 
362
- private async stopInternal(): Promise<void> {
475
+ private async stopInternal(): Promise<INoSqlDbServerStopResult> {
363
476
  const startCompletionPromise = this.startCompletionPromise;
364
477
  if (startCompletionPromise) {
365
478
  await startCompletionPromise;
366
479
  }
367
480
 
368
481
  if (!this.isRunning && !this.bridgeOwned) {
369
- return;
482
+ return noStopRequestResult;
370
483
  }
371
484
 
372
485
  const requestGracefulStop = this.isRunning && this.bridge.running;
486
+ let stopAcknowledged = false;
373
487
  if (requestGracefulStop) {
374
488
  try {
375
489
  await this.bridge.stopDb();
490
+ stopAcknowledged = true;
376
491
  } catch {
377
492
  // Confirmed bridge termination below still releases the owned process.
378
493
  }
379
494
  }
495
+ // An engine that acknowledged the stop exits by itself once its management
496
+ // input closes; it is ended only if it has not within the grace.
497
+ if (stopAcknowledged && this.bridge.engineProcessAlive) {
498
+ this.bridge.endManagementInput();
499
+ await this.bridge.waitForEngineExit(noSqlDbServerStopGraceMs);
500
+ }
501
+ const forcedByServer = this.bridge.engineProcessAlive;
380
502
 
381
- await this.bridge.terminate();
503
+ try {
504
+ await this.bridge.terminate();
505
+ } catch (error) {
506
+ // An engine that has exited no longer runs, as the exit handler would
507
+ // have recorded outside a stop; ownership stays for a retry.
508
+ if (this.bridge.lastEngineExit !== null) {
509
+ this.isRunning = false;
510
+ this.resolvedConnectionUri = '';
511
+ this.resolvedPort = undefined;
512
+ }
513
+ throw error;
514
+ }
382
515
  this.bridgeOwned = false;
383
516
  this.isRunning = false;
384
517
  this.resolvedConnectionUri = '';
385
518
  this.resolvedPort = undefined;
519
+ // The process's exit event may follow the confirmed termination by a tick.
520
+ if (this.bridge.engineProcessAlive) await this.bridge.waitForEngineExit(1_000);
521
+ return stopResult(requestGracefulStop, stopAcknowledged, forcedByServer, this.bridge.lastEngineExit);
386
522
  }
387
523
 
388
524
  /**
@@ -1,4 +1,4 @@
1
1
  // Server module exports
2
- export { NoSqlDbServer } from './classes.nosqldbserver.js';
3
- export type { INoSqlDbServerOptions } from './classes.nosqldbserver.js';
2
+ export { NoSqlDbServer, noSqlDbServerStopGraceMs } from './classes.nosqldbserver.js';
3
+ export type { INoSqlDbServerOptions, INoSqlDbServerStopResult } from './classes.nosqldbserver.js';
4
4
  export type { INoSqlDbQueryOptions, INoSqlDbInboundOptions } from '../rust-db-bridge.js';
@@ -41,7 +41,15 @@ export interface ILocalNoSqlDbAuditedStorageRoot {
41
41
  activeGrants: '0';
42
42
  importStages: '0';
43
43
  };
44
- generations: { manifest: string; onDisk: string[] };
44
+ /**
45
+ * The generation inventory of a root that a 10.x release wrote, as this
46
+ * release's audit reports it and a 10.x engine's audit of its own root does.
47
+ * A root of this release keeps none: its report has no `generations` and
48
+ * says `storageFormat: 'format-3'`.
49
+ */
50
+ generations?: { manifest: string; onDisk: string[] };
51
+ /** `'format-3'` for a root of this release; absent for a root that a 10.x release wrote. */
52
+ storageFormat?: 'format-3';
45
53
  alignmentEligible: boolean;
46
54
  reasons: string[];
47
55
  physicalAdmissionViolations: [];
@@ -189,7 +189,7 @@ export const normalizeNoSqlDbStorageRehearsalReceipt = (value: unknown, input: T
189
189
  || (input.expectedSourceProviderRootId !== undefined && value.sourceProviderRootId !== input.expectedSourceProviderRootId)
190
190
  || !['sourceProviderRootId', 'destinationProviderRootId', 'sourceSnapshotSha256', 'sourceRecordsSha256', 'preservedRecordsSha256', 'receiptSha256'].every((key) => digest(value[key]))
191
191
  || value.sourceProviderRootId === value.destinationProviderRootId
192
- || value.sourceStorageRootSha256 !== plugins.crypto.createHash('sha256').update(`${input.sourceFolderPath}/.__rustdb_mvcc/data`).digest('hex')
192
+ || value.sourceStorageRootSha256 !== plugins.crypto.createHash('sha256').update(`${input.sourceFolderPath}/.nosqldb-store/data`).digest('hex')
193
193
  || value.requestSha256 !== sha256('smartdb.storage-rehearsal.request.v2\0', input)
194
194
  || !integer(value.scramIterations, 4096, 0xffff_ffff)
195
195
  || ![value.destinationDevice, value.destinationInode, value.sourceLsn, value.destinationLsn].every(unsigned)
@@ -119,7 +119,7 @@ const eligibleGenerations = (value: unknown): boolean => {
119
119
  const eligibleInspection = (value: unknown, request: ILocalNoSqlDbStoppedStorageRootAlignmentInput): value is ILocalNoSqlDbAuditedStorageRoot => {
120
120
  if (!data(value, ['status', 'bindingState', 'bindingChange', 'storagePurpose', 'profileSchema', 'providerRootId',
121
121
  'groupId', 'timelineId', 'lsn', 'records', 'recordsSha256', 'recordsWithoutFencingProfileSha256', 'authUsers',
122
- 'authority', 'generations', 'alignmentEligible', 'reasons', 'physicalAdmissionViolations'])
122
+ 'authority', 'alignmentEligible', 'reasons', 'physicalAdmissionViolations'], ['generations', 'storageFormat'])
123
123
  || value.status !== 'audited' || value.alignmentEligible !== true || value.storagePurpose !== 'production'
124
124
  || ![1, 4].includes(value.profileSchema as number) || !sha(value.providerRootId) || !sha(value.recordsSha256)
125
125
  || !sha(value.recordsWithoutFencingProfileSha256) || !lineage(value.groupId) || !lineage(value.timelineId)
@@ -128,7 +128,11 @@ const eligibleInspection = (value: unknown, request: ILocalNoSqlDbStoppedStorage
128
128
  || value.bindingState !== (value.bindingChange === 'none' ? 'matching' : 'foreign')
129
129
  || !data(value.authority, ['resourceFences', 'receipts', 'publicationHolds', 'activeAllocations', 'retiredAllocations',
130
130
  'allocationReplays', 'activeGrants', 'importStages']) || Object.values(value.authority).some(count => count !== '0')
131
- || !eligibleGenerations(value.generations)
131
+ // A format-3 root says so and keeps no generation inventory; any other
132
+ // report is a paged root's and must carry an eligible one.
133
+ || !(value.storageFormat === 'format-3'
134
+ ? !('generations' in value)
135
+ : !('storageFormat' in value) && eligibleGenerations(value.generations))
132
136
  || !Array.isArray(value.reasons) || value.reasons.length || !Array.isArray(value.physicalAdmissionViolations)
133
137
  || value.physicalAdmissionViolations.length) return false;
134
138
  const pins = request.expect;
@@ -239,7 +239,7 @@ export const assertNoLocalNoSqlDbStorageRootRelocationWorkspace = async (
239
239
  storagePathArg: string,
240
240
  ): Promise<void> => {
241
241
  const storagePath = plugins.path.resolve(storagePathArg);
242
- const internalPath = plugins.path.join(storagePath, '.__rustdb_internal');
242
+ const internalPath = plugins.path.join(storagePath, '.nosqldb-control');
243
243
  try {
244
244
  const storage = await plugins.fs.stat(storagePath);
245
245
  if (!storage.isDirectory()) {